init commit
This commit is contained in:
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidProjectSystem">
|
||||||
|
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/compiler.xml
generated
Normal file
6
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
18
.idea/deploymentTargetSelector.xml
generated
Normal file
18
.idea/deploymentTargetSelector.xml
generated
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetSelector">
|
||||||
|
<selectionStates>
|
||||||
|
<SelectionState runConfigName="app">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2025-10-04T02:36:57.095708689Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=LGMG600S9b4da66b" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
|
</SelectionState>
|
||||||
|
</selectionStates>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/gradle.xml
generated
Normal file
19
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
10
.idea/migrations.xml
generated
Normal file
10
.idea/migrations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
.idea/misc.xml
generated
Normal file
9
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
17
.idea/runConfigurations.xml
generated
Normal file
17
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||||
|
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||||
|
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.kotlin/errors/errors-1759321347725.log
Normal file
4
.kotlin/errors/errors-1759321347725.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||||
|
1. Kotlin compile daemon is ready
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759400006923.log
Normal file
4
.kotlin/errors/errors-1759400006923.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||||
|
1. Kotlin compile daemon is ready
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759466115565.log
Normal file
4
.kotlin/errors/errors-1759466115565.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||||
|
1. Kotlin compile daemon is ready
|
||||||
|
|
||||||
4
.kotlin/errors/errors-1759530749084.log
Normal file
4
.kotlin/errors/errors-1759530749084.log
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||||
|
1. Kotlin compile daemon is ready
|
||||||
|
|
||||||
73
app/build-legacy.gradle.kts
Normal file
73
app/build-legacy.gradle.kts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.godeye"
|
||||||
|
compileSdk = 29 // Android 10 для максимальной совместимости
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.example.godeye.legacy"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 28 // Android 9
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0-legacy"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ТОЛЬКО ViewBinding для legacy версии
|
||||||
|
buildFeatures {
|
||||||
|
compose = false
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// МИНИМАЛЬНЫЕ зависимости для Android 9
|
||||||
|
implementation("androidx.core:core-ktx:1.3.2")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.2.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0")
|
||||||
|
|
||||||
|
// UI компоненты для legacy
|
||||||
|
implementation("com.google.android.material:material:1.3.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
||||||
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.1.0")
|
||||||
|
|
||||||
|
// ViewModel для legacy
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0")
|
||||||
|
|
||||||
|
// Сетевые библиотеки
|
||||||
|
implementation("io.socket:socket.io-client:2.1.0")
|
||||||
|
implementation("com.google.code.gson:gson:2.8.9")
|
||||||
|
|
||||||
|
// Корутины
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
id("com.android.application")
|
||||||
alias(libs.plugins.kotlin.android)
|
id("org.jetbrains.kotlin.android")
|
||||||
alias(libs.plugins.kotlin.compose)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.godeye"
|
namespace = "com.example.godeye"
|
||||||
compileSdk = 36
|
compileSdk = 29 // Понижаем до Android 10
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.example.godeye"
|
applicationId = "com.example.godeye"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 28 // Понижаем до Android 9
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0-legacy"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -27,79 +26,62 @@ android {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
compose = true
|
|
||||||
viewBinding = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Исправляем проблему с Java toolchain
|
kotlinOptions {
|
||||||
java {
|
jvmTarget = "1.8"
|
||||||
toolchain {
|
}
|
||||||
languageVersion.set(JavaLanguageVersion.of(17))
|
|
||||||
}
|
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
|
||||||
|
buildFeatures {
|
||||||
|
compose = false
|
||||||
|
viewBinding = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Core Android
|
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для Android 9 (compileSdk 29)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation("androidx.core:core-ktx:1.3.2") // Совместимо с API 29
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation("androidx.appcompat:appcompat:1.2.0") // Совместимо с API 29
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0") // Совместимо с API 29
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
|
||||||
implementation(libs.androidx.compose.ui)
|
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
||||||
implementation(libs.androidx.compose.material3)
|
|
||||||
|
|
||||||
// ViewModel and LiveData
|
// Классический Android UI - версии для API 29
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
implementation("com.google.android.material:material:1.3.0") // Совместимо с API 29
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
implementation("androidx.fragment:fragment-ktx:1.2.5") // Совместимо с API 29
|
||||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
implementation("androidx.cardview:cardview:1.0.0") // Совместимо с API 29
|
||||||
|
implementation("androidx.activity:activity-ktx:1.1.0") // Совместимо с API 29
|
||||||
|
|
||||||
// Socket.IO для WebSocket соединения
|
// СТАРЫЕ ViewModel версии для API 29
|
||||||
implementation("io.socket:socket.io-client:2.1.2")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") // Совместимо с API 29
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") // Совместимо с API 29
|
||||||
|
|
||||||
// Пока уберем WebRTC зависимость - создадим заглушку для демонстрации
|
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
|
||||||
// В реальном проекте нужно будет настроить правильную WebRTC библиотеку
|
// Вместо CameraX используем старую Camera2 API напрямую
|
||||||
|
|
||||||
// Camera2 API
|
// Socket.IO и базовые сетевые библиотеки
|
||||||
implementation("androidx.camera:camera-core:1.3.1")
|
implementation("io.socket:socket.io-client:2.1.0")
|
||||||
implementation("androidx.camera:camera-camera2:1.3.1")
|
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
|
||||||
implementation("androidx.camera:camera-lifecycle:1.3.1")
|
|
||||||
implementation("androidx.camera:camera-view:1.3.1")
|
|
||||||
|
|
||||||
// JSON парсинг
|
// УБИРАЕМ WebRTC полностью для стабильности
|
||||||
implementation("com.google.code.gson:gson:2.10.1")
|
// implementation("io.getstream:stream-webrtc-android:1.0.4")
|
||||||
|
|
||||||
// Корутины
|
// Старые корутины
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год
|
||||||
|
|
||||||
// RecyclerView
|
// Базовые зависимости - старые версии
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год
|
||||||
|
|
||||||
// Work Manager для фоновых задач
|
// УБИРАЕМ Work Manager и Activity KTX
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
// implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||||
|
// implementation("androidx.activity:activity-ktx:1.7.2")
|
||||||
// Permissions
|
|
||||||
implementation("androidx.activity:activity-compose:1.8.2")
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation(libs.junit)
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
||||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
||||||
}
|
}
|
||||||
60
app/src/main/AndroidManifest-legacy.xml
Normal file
60
app/src/main/AndroidManifest-legacy.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Разрешения для Android 9 -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
<!-- Hardware features -->
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".GodEyeApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="28">
|
||||||
|
|
||||||
|
<!-- ТОЛЬКО LegacyMainActivity как точка входа -->
|
||||||
|
<activity
|
||||||
|
android:name=".LegacyMainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- LegacyCameraActivity -->
|
||||||
|
<activity
|
||||||
|
android:name=".LegacyCameraActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
|
<!-- SocketService для legacy -->
|
||||||
|
<service
|
||||||
|
android:name=".services.SocketService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -2,56 +2,67 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- Основные разрешения -->
|
<!-- Разрешения согласно ТЗ -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
|
|
||||||
<!-- Требования к оборудованию -->
|
<!-- Hardware features согласно ТЗ -->
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".GodEyeApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.GodEye">
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
tools:targetApi="31">
|
||||||
|
|
||||||
|
<!-- MainActivity - главный экран согласно ТЗ -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.GodEye"
|
android:theme="@style/Theme.GodEye"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Socket Service для WebSocket соединения -->
|
<!-- LegacyMainActivity - упрощенная версия для Android 9 -->
|
||||||
|
<activity
|
||||||
|
android:name=".LegacyMainActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
|
<!-- LegacyCameraActivity - камера для Android 9 -->
|
||||||
|
<activity
|
||||||
|
android:name=".LegacyCameraActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/Theme.GodEye"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
|
<!-- SocketService - WebSocket соединение согласно ТЗ -->
|
||||||
<service
|
<service
|
||||||
android:name=".services.SocketService"
|
android:name=".services.SocketService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false" />
|
||||||
android:foregroundServiceType="camera" />
|
|
||||||
|
|
||||||
<!-- Camera Service для работы с камерой -->
|
|
||||||
<service
|
|
||||||
android:name=".services.CameraService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="camera" />
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
94
app/src/main/java/com/example/godeye/GodEyeApplication.kt
Normal file
94
app/src/main/java/com/example/godeye/GodEyeApplication.kt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package com.example.godeye
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.example.godeye.utils.ErrorHandler
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GodEyeApplication - главный класс приложения для инициализации глобальных компонентов
|
||||||
|
* Соответствует требованиям ТЗ для правильной инициализации приложения
|
||||||
|
*/
|
||||||
|
class GodEyeApplication : Application() {
|
||||||
|
|
||||||
|
private val errorHandler = ErrorHandler()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
Logger.step("APPLICATION_START", "GodEye Application starting...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Инициализация глобальных компонентов
|
||||||
|
initializeLogging()
|
||||||
|
setupExceptionHandler()
|
||||||
|
|
||||||
|
Logger.step("APPLICATION_READY", "GodEye Application initialized successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("APPLICATION_INIT_ERROR", "Failed to initialize application", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация системы логирования
|
||||||
|
*/
|
||||||
|
private fun initializeLogging() {
|
||||||
|
Logger.step("LOGGING_INIT", "Initializing logging system")
|
||||||
|
// Система логирования уже инициализирована в Logger object
|
||||||
|
Logger.d("Application context available: ${this.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настройка глобального обработчика исключений
|
||||||
|
*/
|
||||||
|
private fun setupExceptionHandler() {
|
||||||
|
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
|
||||||
|
try {
|
||||||
|
// Используем наш ErrorHandler для обработки исключений
|
||||||
|
errorHandler.handleUncaughtException(thread, exception)
|
||||||
|
|
||||||
|
// Специальная обработка известных безопасных ошибок
|
||||||
|
when {
|
||||||
|
// Compose hover events bug - игнорируем
|
||||||
|
exception is IllegalStateException &&
|
||||||
|
exception.message?.contains("ACTION_HOVER_EXIT event was not cleared") == true -> {
|
||||||
|
Logger.d("Ignoring Compose hover event bug")
|
||||||
|
return@setDefaultUncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ошибки при завершении приложения - игнорируем
|
||||||
|
exception is InternalError &&
|
||||||
|
exception.message?.contains("Thread starting during runtime shutdown") == true -> {
|
||||||
|
Logger.d("Ignoring shutdown thread creation error")
|
||||||
|
return@setDefaultUncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebRTC ошибки - логируем но не крашим
|
||||||
|
exception.message?.contains("Failed to set local") == true -> {
|
||||||
|
Logger.error("WEBRTC_ERROR", "WebRTC error handled gracefully", exception)
|
||||||
|
return@setDefaultUncaughtExceptionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для критических ошибок используем стандартный обработчик
|
||||||
|
else -> {
|
||||||
|
Logger.error("CRITICAL_ERROR", "Critical error, delegating to default handler", exception)
|
||||||
|
defaultHandler?.uncaughtException(thread, exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (handlerException: Exception) {
|
||||||
|
// Если наш обработчик тоже упал, используем стандартный
|
||||||
|
Logger.error("HANDLER_ERROR", "Error in exception handler", handlerException)
|
||||||
|
defaultHandler?.uncaughtException(thread, exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("EXCEPTION_HANDLER_SET", "Global exception handler configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTerminate() {
|
||||||
|
Logger.step("APPLICATION_TERMINATE", "GodEye Application terminating...")
|
||||||
|
super.onTerminate()
|
||||||
|
}
|
||||||
|
}
|
||||||
292
app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
Normal file
292
app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package com.example.godeye
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.Camera
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.SurfaceHolder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.example.godeye.databinding.ActivityLegacyCameraBinding
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LegacyCameraActivity - камера для Android 9
|
||||||
|
* Использует устаревший Camera API для максимальной совместимости
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
class LegacyCameraActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLegacyCameraBinding
|
||||||
|
private var camera: Camera? = null
|
||||||
|
private var surfaceHolder: SurfaceHolder? = null
|
||||||
|
private var isPreviewRunning = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_CAMERA_CREATE", "Creating LegacyCameraActivity for Android 9")
|
||||||
|
|
||||||
|
binding = ActivityLegacyCameraBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setupUI()
|
||||||
|
setupCamera()
|
||||||
|
|
||||||
|
Logger.step("LEGACY_CAMERA_CREATE_SUCCESS", "LegacyCameraActivity created successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_CREATE_ERROR", "Error creating LegacyCameraActivity", e)
|
||||||
|
Toast.makeText(this, "Ошибка инициализации камеры", Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUI() {
|
||||||
|
binding.apply {
|
||||||
|
// Настройка кнопок
|
||||||
|
btnBack.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCapture.setOnClickListener {
|
||||||
|
capturePhoto()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSwitchCamera.setOnClickListener {
|
||||||
|
switchCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка информации
|
||||||
|
tvCameraInfo.text = "📹 Legacy Camera для Android 9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupCamera() {
|
||||||
|
try {
|
||||||
|
if (!checkCameraPermission()) {
|
||||||
|
Toast.makeText(this, "Нет разрешения на использование камеры", Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настройка SurfaceView для предварительного просмотра
|
||||||
|
surfaceHolder = binding.surfaceViewCamera.holder
|
||||||
|
surfaceHolder?.addCallback(this)
|
||||||
|
surfaceHolder?.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
|
||||||
|
|
||||||
|
Logger.step("LEGACY_CAMERA_SETUP", "Camera surface setup completed")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_SETUP_ERROR", "Error setting up camera", e)
|
||||||
|
Toast.makeText(this, "Ошибка настройки камеры", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCameraPermission(): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_SURFACE_CREATED", "Camera surface created")
|
||||||
|
startCamera()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_SURFACE_CREATE_ERROR", "Error on surface created", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_SURFACE_CHANGED", "Camera surface changed: ${width}x${height}")
|
||||||
|
|
||||||
|
if (isPreviewRunning) {
|
||||||
|
camera?.stopPreview()
|
||||||
|
}
|
||||||
|
|
||||||
|
startCameraPreview()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_SURFACE_CHANGE_ERROR", "Error on surface changed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_SURFACE_DESTROYED", "Camera surface destroyed")
|
||||||
|
stopCamera()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_SURFACE_DESTROY_ERROR", "Error on surface destroyed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCamera() {
|
||||||
|
try {
|
||||||
|
if (camera == null) {
|
||||||
|
camera = Camera.open()
|
||||||
|
Logger.step("LEGACY_CAMERA_OPENED", "Legacy camera opened successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_OPEN_ERROR", "Error opening camera", e)
|
||||||
|
Toast.makeText(this, "Не удалось открыть камеру", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCameraPreview() {
|
||||||
|
try {
|
||||||
|
camera?.let { cam ->
|
||||||
|
cam.setPreviewDisplay(surfaceHolder)
|
||||||
|
|
||||||
|
// Настройка параметров камеры для Android 9
|
||||||
|
val parameters = cam.parameters
|
||||||
|
val supportedSizes = parameters.supportedPreviewSizes
|
||||||
|
|
||||||
|
// Выбираем подходящий размер превью
|
||||||
|
supportedSizes?.let { sizes ->
|
||||||
|
val optimalSize = getOptimalPreviewSize(sizes, binding.surfaceViewCamera.width, binding.surfaceViewCamera.height)
|
||||||
|
optimalSize?.let {
|
||||||
|
parameters.setPreviewSize(it.width, it.height)
|
||||||
|
cam.parameters = parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cam.startPreview()
|
||||||
|
isPreviewRunning = true
|
||||||
|
|
||||||
|
// Обновляем UI
|
||||||
|
binding.tvStatus.text = "✅ Камера активна"
|
||||||
|
|
||||||
|
Logger.step("LEGACY_CAMERA_PREVIEW_STARTED", "Camera preview started successfully")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Logger.error("LEGACY_CAMERA_PREVIEW_ERROR", "Error starting camera preview", e)
|
||||||
|
Toast.makeText(this, "Ошибка запуска предварительного просмотра", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOptimalPreviewSize(sizes: List<Camera.Size>, width: Int, height: Int): Camera.Size? {
|
||||||
|
val targetRatio = width.toDouble() / height
|
||||||
|
var optimalSize: Camera.Size? = null
|
||||||
|
var minDiff = Double.MAX_VALUE
|
||||||
|
|
||||||
|
for (size in sizes) {
|
||||||
|
val ratio = size.width.toDouble() / size.height
|
||||||
|
if (Math.abs(ratio - targetRatio) > 0.1) continue
|
||||||
|
|
||||||
|
if (Math.abs(size.height - height) < minDiff) {
|
||||||
|
optimalSize = size
|
||||||
|
minDiff = Math.abs(size.height - height).toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optimalSize == null) {
|
||||||
|
minDiff = Double.MAX_VALUE
|
||||||
|
for (size in sizes) {
|
||||||
|
if (Math.abs(size.height - height) < minDiff) {
|
||||||
|
optimalSize = size
|
||||||
|
minDiff = Math.abs(size.height - height).toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopCamera() {
|
||||||
|
try {
|
||||||
|
camera?.let { cam ->
|
||||||
|
if (isPreviewRunning) {
|
||||||
|
cam.stopPreview()
|
||||||
|
isPreviewRunning = false
|
||||||
|
}
|
||||||
|
cam.release()
|
||||||
|
camera = null
|
||||||
|
|
||||||
|
binding.tvStatus.text = "⚪ Камера остановлена"
|
||||||
|
|
||||||
|
Logger.step("LEGACY_CAMERA_STOPPED", "Camera stopped and released")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_STOP_ERROR", "Error stopping camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun capturePhoto() {
|
||||||
|
try {
|
||||||
|
if (!isPreviewRunning) {
|
||||||
|
Toast.makeText(this, "Камера не активна", Toast.LENGTH_SHORT).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("LEGACY_CAMERA_CAPTURE", "Capturing photo with legacy camera")
|
||||||
|
|
||||||
|
// Простая реализация захвата фото
|
||||||
|
camera?.takePicture(null, null) { data, _ ->
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_PHOTO_CAPTURED", "Photo captured, size: ${data.size} bytes")
|
||||||
|
Toast.makeText(this@LegacyCameraActivity, "Фото сделано!", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// Здесь можно добавить сохранение фото или отправку на сервер
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_PHOTO_SAVE_ERROR", "Error processing captured photo", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_CAPTURE_ERROR", "Error capturing photo", e)
|
||||||
|
Toast.makeText(this, "Ошибка съемки фото", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun switchCamera() {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_CAMERA_SWITCH", "Attempting to switch camera")
|
||||||
|
|
||||||
|
// Для Android 9 просто показываем сообщение
|
||||||
|
Toast.makeText(this, "Переключение камеры (в разработке)", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_SWITCH_ERROR", "Error switching camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
try {
|
||||||
|
if (isPreviewRunning) {
|
||||||
|
camera?.stopPreview()
|
||||||
|
isPreviewRunning = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_PAUSE_ERROR", "Error pausing camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
try {
|
||||||
|
if (camera != null && !isPreviewRunning) {
|
||||||
|
startCameraPreview()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_RESUME_ERROR", "Error resuming camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
try {
|
||||||
|
stopCamera()
|
||||||
|
Logger.step("LEGACY_CAMERA_DESTROY", "LegacyCameraActivity destroyed safely")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_DESTROY_ERROR", "Error destroying camera activity", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
240
app/src/main/java/com/example/godeye/LegacyMainActivity.kt
Normal file
240
app/src/main/java/com/example/godeye/LegacyMainActivity.kt
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
package com.example.godeye
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.example.godeye.databinding.ActivityLegacyMainBinding
|
||||||
|
import com.example.godeye.services.SocketService
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LegacyMainActivity - упрощенная версия для Android 9
|
||||||
|
* Использует классические Android Views вместо Compose
|
||||||
|
* Максимальная совместимость с Android 9
|
||||||
|
*/
|
||||||
|
class LegacyMainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLegacyMainBinding
|
||||||
|
private var socketService: SocketService? = null
|
||||||
|
private var isServiceBound = false
|
||||||
|
|
||||||
|
// Подключение к SocketService
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
Logger.step("LEGACY_SERVICE_CONNECTED", "SocketService connected to LegacyMainActivity")
|
||||||
|
val binder = service as? SocketService.LocalBinder
|
||||||
|
socketService = binder?.getService()
|
||||||
|
isServiceBound = true
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Logger.step("LEGACY_SERVICE_DISCONNECTED", "SocketService disconnected")
|
||||||
|
socketService = null
|
||||||
|
isServiceBound = false
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка разрешений
|
||||||
|
private val permissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
val allGranted = permissions.values.all { it }
|
||||||
|
if (allGranted) {
|
||||||
|
Logger.step("LEGACY_PERMISSIONS_GRANTED", "All permissions granted")
|
||||||
|
updateUI()
|
||||||
|
} else {
|
||||||
|
Logger.step("LEGACY_PERMISSIONS_DENIED", "Some permissions denied")
|
||||||
|
Toast.makeText(this, "Требуются разрешения для работы приложения", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_ACTIVITY_CREATE", "LegacyMainActivity onCreate for Android 9")
|
||||||
|
|
||||||
|
binding = ActivityLegacyMainBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setupUI()
|
||||||
|
checkPermissions()
|
||||||
|
startAndBindService()
|
||||||
|
|
||||||
|
Logger.step("LEGACY_ACTIVITY_CREATE_SUCCESS", "LegacyMainActivity created successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_ACTIVITY_CREATE_ERROR", "Error creating LegacyMainActivity", e)
|
||||||
|
Toast.makeText(this, "Ошибка запуска приложения", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUI() {
|
||||||
|
binding.apply {
|
||||||
|
// Настройка кнопок
|
||||||
|
btnConnect.setOnClickListener {
|
||||||
|
connectToServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnDisconnect.setOnClickListener {
|
||||||
|
disconnectFromServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnCamera.setOnClickListener {
|
||||||
|
openCamera()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSettings.setOnClickListener {
|
||||||
|
openSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Установка начального состояния
|
||||||
|
updateUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissions() {
|
||||||
|
val requiredPermissions = arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO,
|
||||||
|
Manifest.permission.INTERNET,
|
||||||
|
Manifest.permission.ACCESS_NETWORK_STATE
|
||||||
|
)
|
||||||
|
|
||||||
|
val missingPermissions = requiredPermissions.filter { permission ->
|
||||||
|
ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingPermissions.isNotEmpty()) {
|
||||||
|
Logger.step("LEGACY_REQUEST_PERMISSIONS", "Requesting permissions: ${missingPermissions.joinToString()}")
|
||||||
|
permissionLauncher.launch(missingPermissions.toTypedArray())
|
||||||
|
} else {
|
||||||
|
Logger.step("LEGACY_PERMISSIONS_OK", "All permissions already granted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAndBindService() {
|
||||||
|
try {
|
||||||
|
val intent = Intent(this, SocketService::class.java)
|
||||||
|
startForegroundService(intent)
|
||||||
|
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
Logger.step("LEGACY_SERVICE_BIND", "Binding to SocketService")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_SERVICE_BIND_ERROR", "Error binding to service", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUI() {
|
||||||
|
binding.apply {
|
||||||
|
// Обновление статуса подключения
|
||||||
|
if (isServiceBound && socketService != null) {
|
||||||
|
tvStatus.text = "✅ Сервис подключен"
|
||||||
|
btnConnect.isEnabled = true
|
||||||
|
btnCamera.isEnabled = hasAllPermissions()
|
||||||
|
} else {
|
||||||
|
tvStatus.text = "❌ Сервис не подключен"
|
||||||
|
btnConnect.isEnabled = false
|
||||||
|
btnCamera.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление информации об устройстве
|
||||||
|
val deviceInfo = "📱 ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}\n" +
|
||||||
|
"🤖 Android ${android.os.Build.VERSION.RELEASE}"
|
||||||
|
tvDeviceInfo.text = deviceInfo
|
||||||
|
|
||||||
|
// Обновление статуса разрешений
|
||||||
|
val permissionsStatus = if (hasAllPermissions()) {
|
||||||
|
"✅ Разрешения предоставлены"
|
||||||
|
} else {
|
||||||
|
"⚠️ Требуются разрешения"
|
||||||
|
}
|
||||||
|
tvPermissions.text = permissionsStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasAllPermissions(): Boolean {
|
||||||
|
val requiredPermissions = arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO,
|
||||||
|
Manifest.permission.INTERNET,
|
||||||
|
Manifest.permission.ACCESS_NETWORK_STATE
|
||||||
|
)
|
||||||
|
|
||||||
|
return requiredPermissions.all { permission ->
|
||||||
|
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectToServer() {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_CONNECT_SERVER", "Attempting to connect to server")
|
||||||
|
// Простая заглушка для подключения
|
||||||
|
binding.tvConnectionStatus.text = "🔄 Подключение к серверу..."
|
||||||
|
Toast.makeText(this, "Подключение к серверу...", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CONNECT_ERROR", "Error connecting to server", e)
|
||||||
|
binding.tvConnectionStatus.text = "❌ Ошибка подключения"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnectFromServer() {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_DISCONNECT_SERVER", "Disconnecting from server")
|
||||||
|
binding.tvConnectionStatus.text = "⚪ Отключено"
|
||||||
|
Toast.makeText(this, "Отключено от сервера", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_DISCONNECT_ERROR", "Error disconnecting", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openCamera() {
|
||||||
|
if (!hasAllPermissions()) {
|
||||||
|
Toast.makeText(this, "Требуются разрешения камеры", Toast.LENGTH_SHORT).show()
|
||||||
|
checkPermissions()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_OPEN_CAMERA", "Opening legacy camera")
|
||||||
|
val intent = Intent(this, LegacyCameraActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_CAMERA_ERROR", "Error opening camera", e)
|
||||||
|
Toast.makeText(this, "Ошибка открытия камеры", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openSettings() {
|
||||||
|
try {
|
||||||
|
Logger.step("LEGACY_OPEN_SETTINGS", "Opening settings")
|
||||||
|
Toast.makeText(this, "Настройки (в разработке)", Toast.LENGTH_SHORT).show()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_SETTINGS_ERROR", "Error opening settings", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
try {
|
||||||
|
if (isServiceBound) {
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
isServiceBound = false
|
||||||
|
}
|
||||||
|
Logger.step("LEGACY_ACTIVITY_DESTROY", "LegacyMainActivity destroyed safely")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("LEGACY_DESTROY_ERROR", "Error destroying activity", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +1,431 @@
|
|||||||
package com.example.godeye
|
package com.example.godeye
|
||||||
|
|
||||||
import android.Manifest
|
import android.content.ComponentName
|
||||||
import android.content.pm.PackageManager
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.godeye.camera.CameraScreen
|
||||||
import com.example.godeye.managers.PermissionManager
|
import com.example.godeye.managers.PermissionManager
|
||||||
import com.example.godeye.ui.screens.MainScreen
|
import com.example.godeye.models.*
|
||||||
|
import com.example.godeye.services.SocketService
|
||||||
|
import com.example.godeye.ui.components.*
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
import com.example.godeye.ui.theme.GodEyeTheme
|
import com.example.godeye.ui.theme.GodEyeTheme
|
||||||
import com.example.godeye.ui.viewmodels.MainViewModel
|
import com.example.godeye.utils.ErrorHandler
|
||||||
import com.example.godeye.utils.Logger
|
import com.example.godeye.utils.Logger
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MainActivity - упрощенная версия для Android 9
|
||||||
|
* БЕЗ сложных анимаций и градиентов
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModels()
|
||||||
private lateinit var permissionManager: PermissionManager
|
private val errorHandler = ErrorHandler()
|
||||||
|
private var socketService: SocketService? = null
|
||||||
|
|
||||||
// Launcher для запроса разрешений
|
// Подключение к SocketService
|
||||||
private val permissionsLauncher = registerForActivityResult(
|
private val serviceConnection = object : ServiceConnection {
|
||||||
ActivityResultContracts.RequestMultiplePermissions()
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
) { permissions ->
|
Logger.step("SERVICE_CONNECTED", "SocketService connected to MainActivity")
|
||||||
val allGranted = permissions.values.all { it }
|
val binder = service as SocketService.LocalBinder
|
||||||
if (allGranted) {
|
socketService = binder.getService()
|
||||||
Logger.d("All permissions granted")
|
viewModel.bindToSocketService(socketService!!)
|
||||||
viewModel.startServices() // Запуск сервисов после получения разрешений
|
|
||||||
} else {
|
|
||||||
Logger.w("Some permissions were denied")
|
|
||||||
val deniedPermissions = permissions.filterValues { !it }.keys
|
|
||||||
Logger.w("Denied permissions: ${deniedPermissions.joinToString(", ")}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Логируем статус разрешений
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
permissionManager.logPermissionsStatus()
|
Logger.step("SERVICE_DISCONNECTED", "SocketService disconnected from MainActivity")
|
||||||
|
socketService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка разрешений
|
||||||
|
private val permissionLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
Logger.step("PERMISSIONS_RESULT", "Permission request result received")
|
||||||
|
val allGranted = permissions.values.all { it }
|
||||||
|
if (allGranted) {
|
||||||
|
Logger.step("PERMISSIONS_ALL_GRANTED", "All permissions granted")
|
||||||
|
viewModel.onPermissionsGranted()
|
||||||
|
} else {
|
||||||
|
val denied = permissions.filterValues { !it }.keys
|
||||||
|
Logger.step("PERMISSIONS_DENIED", "Some permissions denied: ${denied.joinToString()}")
|
||||||
|
val permissionManager = PermissionManager(this)
|
||||||
|
val hasCritical = denied.any { it in PermissionManager.CRITICAL_PERMISSIONS }
|
||||||
|
if (hasCritical) {
|
||||||
|
errorHandler.handleError(AppError.CameraPermissionDenied, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Logger.d("MainActivity created")
|
try {
|
||||||
permissionManager = PermissionManager(this)
|
Logger.step("ACTIVITY_CREATE", "MainActivity onCreate simplified for Android 9")
|
||||||
|
Logger.d("Device: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
|
||||||
|
Logger.d("Android: ${android.os.Build.VERSION.RELEASE}")
|
||||||
|
|
||||||
// Проверяем разрешения при запуске
|
// Запуск SocketService
|
||||||
checkAndRequestPermissions()
|
startAndBindSocketService()
|
||||||
if (permissionManager.hasAllRequiredPermissions()) {
|
|
||||||
viewModel.startServices() // Запуск сервисов если разрешения уже есть
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
GodEyeTheme {
|
GodEyeTheme {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
var showCamera by remember { mutableStateOf(false) }
|
||||||
|
val cameraRequest by viewModel.cameraRequest.collectAsState()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Surface(
|
// Автоматическое принятие запросов камеры
|
||||||
modifier = Modifier.fillMaxSize(),
|
LaunchedEffect(cameraRequest) {
|
||||||
color = MaterialTheme.colorScheme.background
|
val currentRequest = cameraRequest
|
||||||
) {
|
if (currentRequest != null) {
|
||||||
Scaffold(
|
Logger.step("AUTO_ACCEPT_CAMERA_REQUEST", "Auto-accepting camera request")
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
showCamera = true
|
||||||
) { paddingValues ->
|
viewModel.acceptCameraRequest(currentRequest.sessionId, "Auto-accepted")
|
||||||
MainScreen(
|
} else {
|
||||||
viewModel = viewModel,
|
showCamera = false
|
||||||
onRequestPermissions = {
|
}
|
||||||
requestMissingPermissions()
|
}
|
||||||
},
|
|
||||||
onShowError = { message ->
|
// Обработка ошибок
|
||||||
coroutineScope.launch {
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
snackbarHostState.showSnackbar(message)
|
LaunchedEffect(connectionState) {
|
||||||
}
|
if (connectionState == ConnectionState.ERROR) {
|
||||||
|
errorHandler.handleError(AppError.NetworkError, this@MainActivity, scope, snackbarHostState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
when {
|
||||||
|
showCamera && cameraRequest != null -> {
|
||||||
|
Logger.step("UI_RENDERING_CAMERA", "Rendering simplified CameraScreen")
|
||||||
|
CameraScreen(
|
||||||
|
onBackPressed = {
|
||||||
|
Logger.step("CAMERA_BACK_PRESSED", "User pressed back")
|
||||||
|
showCamera = false
|
||||||
|
viewModel.clearCameraRequest()
|
||||||
|
},
|
||||||
|
sessionId = cameraRequest!!.sessionId,
|
||||||
|
operatorId = cameraRequest!!.operatorId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
showSettings -> {
|
||||||
|
Logger.step("UI_RENDERING_SETTINGS", "Rendering SettingsScreen")
|
||||||
|
SettingsScreen(
|
||||||
|
onBackPressed = { showSettings = false },
|
||||||
|
onServerConfigSaved = { url ->
|
||||||
|
viewModel.updateServerUrl(url)
|
||||||
|
showSettings = false
|
||||||
|
viewModel.connectToServer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Logger.step("UI_RENDERING_MAIN", "Rendering simplified MainScreen")
|
||||||
|
SimplifiedMainScreen(
|
||||||
|
onSettingsClick = { showSettings = true },
|
||||||
|
onCameraAccept = { showCamera = true },
|
||||||
|
snackbarHostState = snackbarHostState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простой Snackbar для ошибок
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = snackbarHostState,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка разрешений
|
||||||
|
checkRequiredPermissions()
|
||||||
|
Logger.step("ACTIVITY_CREATE_COMPLETE", "MainActivity simplified onCreate complete")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("ACTIVITY_CREATE_ERROR", "Error in MainActivity onCreate", e)
|
||||||
|
errorHandler.handleError(AppError.UnknownError(e), this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAndBindSocketService() {
|
||||||
|
Logger.step("SOCKET_SERVICE_START", "Starting SocketService")
|
||||||
|
val intent = Intent(this, SocketService::class.java)
|
||||||
|
startForegroundService(intent)
|
||||||
|
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRequiredPermissions() {
|
||||||
|
Logger.step("PERMISSION_CHECK", "Checking permissions")
|
||||||
|
val permissionManager = PermissionManager(this)
|
||||||
|
if (!permissionManager.checkPermissions()) {
|
||||||
|
val missingPermissions = permissionManager.getMissingPermissions()
|
||||||
|
Logger.step("PERMISSIONS_MISSING", "Requesting: ${missingPermissions.joinToString()}")
|
||||||
|
permissionLauncher.launch(missingPermissions)
|
||||||
|
} else {
|
||||||
|
Logger.step("PERMISSIONS_OK", "All permissions granted")
|
||||||
|
viewModel.onPermissionsGranted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SimplifiedMainScreen(
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
|
onCameraAccept: () -> Unit,
|
||||||
|
snackbarHostState: SnackbarHostState
|
||||||
|
) {
|
||||||
|
val connectionState by viewModel.connectionState.collectAsState()
|
||||||
|
val serverUrl by viewModel.serverUrl.collectAsState()
|
||||||
|
val deviceId by viewModel.deviceId.collectAsState()
|
||||||
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
|
val cameraRequest by viewModel.cameraRequest.collectAsState()
|
||||||
|
val isStreaming by viewModel.isStreaming.collectAsState()
|
||||||
|
val activeSessions by viewModel.activeSessions.collectAsState()
|
||||||
|
val permissionsGranted by viewModel.permissionsGranted.collectAsState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// УПРОЩЕННЫЙ UI ДЛЯ ANDROID 9 - БЕЗ СЛОЖНЫХ АНИМАЦИЙ
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Простой заголовок
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Gray.copy(alpha = 0.3f))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "GodEye Signal Center",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Android Client v1.0 (Simplified)",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разрешения
|
||||||
|
if (!permissionsGranted) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Red.copy(alpha = 0.7f))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("⚠️ Требуются разрешения", color = Color.White)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val permissionManager = PermissionManager(this@MainActivity)
|
||||||
|
permissionLauncher.launch(permissionManager.getMissingPermissions())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Предоставить разрешения")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device ID
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Blue.copy(alpha = 0.3f))
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text("📱 Device ID", color = Color.White)
|
||||||
|
Text(deviceId.take(16) + "...", color = Color.Gray)
|
||||||
|
Text("${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}", color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подключение к серверу
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = when (connectionState) {
|
||||||
|
ConnectionState.CONNECTED -> Color.Green.copy(alpha = 0.3f)
|
||||||
|
ConnectionState.ERROR -> Color.Red.copy(alpha = 0.3f)
|
||||||
|
else -> Color.Yellow.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text("🌐 Сервер", color = Color.White)
|
||||||
|
Text(
|
||||||
|
when (connectionState) {
|
||||||
|
ConnectionState.CONNECTED -> "✅ Подключено"
|
||||||
|
ConnectionState.CONNECTING -> "🔄 Подключение..."
|
||||||
|
ConnectionState.ERROR -> "❌ Ошибка"
|
||||||
|
else -> "⚪ Отключено"
|
||||||
|
},
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
Text("$serverUrl", color = Color.Gray)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if (connectionState == ConnectionState.DISCONNECTED) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (permissionsGranted) {
|
||||||
|
viewModel.connectToServer()
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar("Нужны разрешения")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(if (isLoading) "Подключение..." else "🔗 Подключиться")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::disconnect,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("❌ Отключиться")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Статус трансляции
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (isStreaming) Color.Green.copy(alpha = 0.3f) else Color.Gray.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text("📹 Трансляция", color = Color.White)
|
||||||
|
Text(
|
||||||
|
if (isStreaming) "🔴 Активна: ${activeSessions.size} сессий" else "⚪ Неактивна",
|
||||||
|
color = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопки управления
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onCameraAccept,
|
||||||
|
enabled = permissionsGranted,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("📷 Камера")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = onSettingsClick,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("⚙️ Настройки")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка для запуска Legacy версии
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
Logger.step("LAUNCH_LEGACY_VERSION", "Launching LegacyMainActivity")
|
||||||
|
val intent = Intent(this@MainActivity, LegacyMainActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color(0xFF9C27B0) // Фиолетовый цвет для выделения
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("📱 Legacy версия (Android 9)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрос от оператора
|
||||||
|
cameraRequest?.let { request ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.Yellow.copy(alpha = 0.8f))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("📞 Запрос от оператора", color = Color.Black)
|
||||||
|
Text("Сессия: ${request.sessionId.take(8)}...", color = Color.Black)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.acceptCameraRequest(request.sessionId, "Принято")
|
||||||
|
onCameraAccept()
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("✅ Принять")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.rejectCameraRequest(request.sessionId, "Отклонено")
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("❌ Отклонить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить и запросить недостающие разрешения
|
|
||||||
*/
|
|
||||||
private fun checkAndRequestPermissions() {
|
|
||||||
if (!permissionManager.hasAllRequiredPermissions()) {
|
|
||||||
Logger.d("Some permissions are missing, requesting...")
|
|
||||||
requestMissingPermissions()
|
|
||||||
} else {
|
|
||||||
Logger.d("All required permissions are granted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запросить недостающие разрешения
|
|
||||||
*/
|
|
||||||
private fun requestMissingPermissions() {
|
|
||||||
val missingPermissions = permissionManager.getMissingPermissions()
|
|
||||||
if (missingPermissions.isNotEmpty()) {
|
|
||||||
Logger.d("Requesting permissions: ${missingPermissions.joinToString(", ")}")
|
|
||||||
permissionsLauncher.launch(missingPermissions.toTypedArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
Logger.step("ACTIVITY_DESTROY", "MainActivity destroyed")
|
||||||
|
try {
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UNBIND_SERVICE_ERROR", "Error unbinding SocketService", e)
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Logger.d("MainActivity destroyed")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
500
app/src/main/java/com/example/godeye/MainViewModel.kt
Normal file
500
app/src/main/java/com/example/godeye/MainViewModel.kt
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
package com.example.godeye
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.example.godeye.managers.*
|
||||||
|
import com.example.godeye.models.*
|
||||||
|
import com.example.godeye.services.SocketService
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import com.example.godeye.utils.generateDeviceId
|
||||||
|
import com.example.godeye.utils.getPreferences
|
||||||
|
import com.example.godeye.webrtc.WebRTCManager
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MainViewModel - Главная ViewModel, интегрирующая все компоненты согласно ТЗ
|
||||||
|
* Архитектура: MVVM с LiveData/StateFlow
|
||||||
|
* Сеть: Socket.IO для сигнализации, WebRTC для медиа
|
||||||
|
*/
|
||||||
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val context = getApplication<Application>()
|
||||||
|
|
||||||
|
// Управляющие компоненты согласно ТЗ
|
||||||
|
private var socketService: SocketService? = null
|
||||||
|
private var sessionManager: SessionManager = SessionManager()
|
||||||
|
private var permissionManager: PermissionManager = PermissionManager(context)
|
||||||
|
private var camera2Manager: Camera2Manager = Camera2Manager(context)
|
||||||
|
private var webRTCManager: WebRTCManager? = null
|
||||||
|
|
||||||
|
// Состояния приложения
|
||||||
|
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||||
|
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
|
private val _serverUrl = MutableStateFlow("")
|
||||||
|
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
||||||
|
|
||||||
|
private val _deviceId = MutableStateFlow("")
|
||||||
|
val deviceId: StateFlow<String> = _deviceId.asStateFlow()
|
||||||
|
|
||||||
|
private val _isLoading = MutableStateFlow(false)
|
||||||
|
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||||
|
|
||||||
|
// Управление сессиями согласно ТЗ
|
||||||
|
private val _cameraRequest = MutableStateFlow<CameraRequest?>(null)
|
||||||
|
val cameraRequest: StateFlow<CameraRequest?> = _cameraRequest.asStateFlow()
|
||||||
|
|
||||||
|
private val _activeSessions = MutableStateFlow<Map<String, SessionInfo>>(emptyMap())
|
||||||
|
val activeSessions: StateFlow<Map<String, SessionInfo>> = _activeSessions.asStateFlow()
|
||||||
|
|
||||||
|
private val _isStreaming = MutableStateFlow(false)
|
||||||
|
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
|
||||||
|
|
||||||
|
// Разрешения согласно ТЗ
|
||||||
|
val permissionsGranted = permissionManager.permissionsGranted
|
||||||
|
val missingPermissions = permissionManager.missingPermissions
|
||||||
|
|
||||||
|
// Камеры согласно ТЗ
|
||||||
|
val availableCameras = camera2Manager.availableCameras
|
||||||
|
val cameraState = camera2Manager.cameraState
|
||||||
|
|
||||||
|
// Подключение к SocketService
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
Logger.step("SERVICE_CONNECTED", "SocketService connected")
|
||||||
|
val binder = service as SocketService.LocalBinder
|
||||||
|
socketService = binder.getService()
|
||||||
|
setupServiceObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Logger.step("SERVICE_DISCONNECTED", "SocketService disconnected")
|
||||||
|
socketService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Logger.step("VIEWMODEL_INIT", "MainViewModel initialization with full ТЗ architecture")
|
||||||
|
initializeApp()
|
||||||
|
bindToSocketService()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация приложения согласно ТЗ
|
||||||
|
*/
|
||||||
|
private fun initializeApp() {
|
||||||
|
Logger.step("APP_INIT", "Initializing application with ТЗ requirements")
|
||||||
|
|
||||||
|
// 1. Проверка разрешений (CAMERA, RECORD_AUDIO, INTERNET, FOREGROUND_SERVICE)
|
||||||
|
permissionManager.checkPermissions()
|
||||||
|
|
||||||
|
// 2. Генерация/загрузки Device ID
|
||||||
|
val prefs = context.getPreferences()
|
||||||
|
var deviceId = prefs.getString("device_id", null)
|
||||||
|
if (deviceId == null) {
|
||||||
|
deviceId = generateDeviceId()
|
||||||
|
prefs.edit { putString("device_id", deviceId) }
|
||||||
|
}
|
||||||
|
_deviceId.value = deviceId
|
||||||
|
|
||||||
|
// 3. Загрузка сохраненного URL сервера
|
||||||
|
val savedUrl = prefs.getString("server_url", "http://192.168.219.108:3001") ?: ""
|
||||||
|
_serverUrl.value = savedUrl
|
||||||
|
|
||||||
|
// 4. Инициализация WebRTC
|
||||||
|
initializeWebRTC()
|
||||||
|
|
||||||
|
Logger.step("APP_INIT_COMPLETE", "Application initialized according to ТЗ")
|
||||||
|
Logger.d("Configuration:")
|
||||||
|
Logger.d(" Device ID: $deviceId")
|
||||||
|
Logger.d(" Server URL: $savedUrl")
|
||||||
|
Logger.d(" Available cameras: ${camera2Manager.getAvailableCameraTypes()}")
|
||||||
|
Logger.d(" Permissions granted: ${permissionManager.permissionsGranted.value}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подключение к SocketService для фоновой работы
|
||||||
|
*/
|
||||||
|
private fun bindToSocketService() {
|
||||||
|
val intent = Intent(context, SocketService::class.java)
|
||||||
|
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Настройка наблюдателей за SocketService
|
||||||
|
*/
|
||||||
|
private fun setupServiceObservers() {
|
||||||
|
val service = socketService ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за состоянием подключения
|
||||||
|
service.connectionState.collect { state ->
|
||||||
|
_connectionState.value = state
|
||||||
|
Logger.step("CONNECTION_STATE_CHANGED", "Connection state: $state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за запросами камеры от операторов
|
||||||
|
service.cameraRequests.collect { request ->
|
||||||
|
if (request != null) {
|
||||||
|
Logger.step("CAMERA_REQUEST_RECEIVED",
|
||||||
|
"Camera request from ${request.operatorId} for ${request.cameraType}")
|
||||||
|
_cameraRequest.value = request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за WebRTC событиями
|
||||||
|
service.webRTCEvents.collect { event ->
|
||||||
|
event?.let { handleWebRTCEvent(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за сессиями
|
||||||
|
sessionManager.sessions.collect { sessions ->
|
||||||
|
val sessionInfo = sessions.mapValues { (sessionId, session) ->
|
||||||
|
SessionInfo(
|
||||||
|
sessionId = sessionId,
|
||||||
|
deviceId = _deviceId.value,
|
||||||
|
operatorId = session.operatorId,
|
||||||
|
cameraType = session.cameraType,
|
||||||
|
status = if (session.webRTCConnected) "Connected" else "Connecting",
|
||||||
|
createdAt = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
.format(java.util.Date(session.startTime))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_activeSessions.value = sessionInfo
|
||||||
|
_isStreaming.value = sessions.values.any { it.isActive }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация WebRTC согласно ТЗ
|
||||||
|
*/
|
||||||
|
private fun initializeWebRTC() {
|
||||||
|
webRTCManager = WebRTCManager(context) { message ->
|
||||||
|
// Обработка сигнальных сообщений через SocketService
|
||||||
|
Logger.step("WEBRTC_SIGNALING", "WebRTC signaling message: ${message.getString("type")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подключение к серверу согласно ТЗ (Socket.IO)
|
||||||
|
*/
|
||||||
|
fun connectToServer() {
|
||||||
|
Logger.step("CONNECT_TO_SERVER", "Connecting to backend server via Socket.IO")
|
||||||
|
|
||||||
|
if (!permissionManager.checkCriticalPermissions()) {
|
||||||
|
Logger.error("CONNECT_FAILED", "Critical permissions not granted", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = _serverUrl.value
|
||||||
|
if (url.isBlank()) {
|
||||||
|
Logger.error("CONNECT_FAILED", "Server URL is empty", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading.value = true
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// Запуск SocketService для фоновой работы
|
||||||
|
val intent = Intent(context, SocketService::class.java)
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
|
||||||
|
// Подключение через SocketService
|
||||||
|
socketService?.connect(url, _deviceId.value)
|
||||||
|
|
||||||
|
// Сохранение URL
|
||||||
|
context.getPreferences().edit {
|
||||||
|
putString("server_url", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("CONNECT_INITIATED", "Connection initiated to: $url")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CONNECT_ERROR", "Failed to connect to server", e)
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
} finally {
|
||||||
|
_isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принятие запроса камеры согласно ТЗ
|
||||||
|
*/
|
||||||
|
fun acceptCameraRequest(sessionId: String, reason: String = "Accepted by user") {
|
||||||
|
Logger.step("ACCEPT_CAMERA_REQUEST", "Accepting camera request: $sessionId")
|
||||||
|
|
||||||
|
val request = _cameraRequest.value
|
||||||
|
if (request?.sessionId != sessionId) {
|
||||||
|
Logger.error("ACCEPT_FAILED", "Invalid session ID", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
// 1. Создание сессии в SessionManager
|
||||||
|
sessionManager.createSession(sessionId, request.operatorId, request.cameraType)
|
||||||
|
|
||||||
|
// 2. Отправка положительного ответа через SocketService
|
||||||
|
socketService?.sendCameraResponse(sessionId, true, reason)
|
||||||
|
|
||||||
|
// 3. Инициализация WebRTC соединения
|
||||||
|
webRTCManager?.startStreaming(sessionId, request.cameraType)
|
||||||
|
|
||||||
|
// 4. Очистка запроса
|
||||||
|
_cameraRequest.value = null
|
||||||
|
|
||||||
|
Logger.step("CAMERA_REQUEST_ACCEPTED", "Camera request accepted for session: $sessionId")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("ACCEPT_REQUEST_ERROR", "Failed to accept camera request", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отклонение запроса камеры
|
||||||
|
*/
|
||||||
|
fun rejectCameraRequest(sessionId: String, reason: String = "Rejected by user") {
|
||||||
|
Logger.step("REJECT_CAMERA_REQUEST", "Rejecting camera request: $sessionId")
|
||||||
|
|
||||||
|
socketService?.sendCameraResponse(sessionId, false, reason)
|
||||||
|
_cameraRequest.value = null
|
||||||
|
|
||||||
|
Logger.step("CAMERA_REQUEST_REJECTED", "Camera request rejected: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка WebRTC событий
|
||||||
|
*/
|
||||||
|
private fun handleWebRTCEvent(event: com.example.godeye.services.WebRTCEvent) {
|
||||||
|
when (event) {
|
||||||
|
is com.example.godeye.services.WebRTCEvent.Offer -> {
|
||||||
|
Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: ${event.sessionId}")
|
||||||
|
webRTCManager?.handleOffer(event.sessionId, event.offer)
|
||||||
|
}
|
||||||
|
is com.example.godeye.services.WebRTCEvent.Answer -> {
|
||||||
|
Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: ${event.sessionId}")
|
||||||
|
webRTCManager?.handleAnswer(event.sessionId, event.answer)
|
||||||
|
}
|
||||||
|
is com.example.godeye.services.WebRTCEvent.IceCandidate -> {
|
||||||
|
Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: ${event.sessionId}")
|
||||||
|
webRTCManager?.handleIceCandidate(event.sessionId, event.candidate, event.sdpMid, event.sdpMLineIndex)
|
||||||
|
}
|
||||||
|
is com.example.godeye.services.WebRTCEvent.SwitchCamera -> {
|
||||||
|
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: ${event.cameraType}")
|
||||||
|
switchCamera(event.cameraType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановка всех стримов
|
||||||
|
*/
|
||||||
|
fun stopAllStreaming() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
Logger.step("STOP_ALL_STREAMING", "Stopping all camera streaming")
|
||||||
|
|
||||||
|
webRTCManager?.stopAllStreaming()
|
||||||
|
|
||||||
|
_activeSessions.value = emptyMap()
|
||||||
|
_isStreaming.value = false
|
||||||
|
|
||||||
|
Logger.step("STOP_ALL_STREAMING_SUCCESS", "All streaming stopped successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("STOP_ALL_STREAMING_ERROR", "Failed to stop streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение камеры
|
||||||
|
*/
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
Logger.step("SWITCH_CAMERA", "Switching camera to: $cameraType")
|
||||||
|
|
||||||
|
webRTCManager?.switchCamera(cameraType)
|
||||||
|
|
||||||
|
// Обновляем тип камеры в активных сессиях
|
||||||
|
val updatedSessions = _activeSessions.value.mapValues { (_, sessionInfo) ->
|
||||||
|
sessionInfo.copy(cameraType = cameraType)
|
||||||
|
}
|
||||||
|
_activeSessions.value = updatedSessions
|
||||||
|
|
||||||
|
Logger.step("SWITCH_CAMERA_SUCCESS", "Camera switched to: $cameraType")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("SWITCH_CAMERA_ERROR", "Failed to switch camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Завершение сессии
|
||||||
|
*/
|
||||||
|
fun endCameraSession(sessionId: String) {
|
||||||
|
Logger.step("END_SESSION", "Ending camera session: $sessionId")
|
||||||
|
|
||||||
|
sessionManager.endSession(sessionId, "Ended by user")
|
||||||
|
webRTCManager?.endSession(sessionId)
|
||||||
|
|
||||||
|
Logger.step("SESSION_ENDED", "Session ended: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отключение от сервера
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
Logger.step("DISCONNECT", "Disconnecting from server")
|
||||||
|
|
||||||
|
socketService?.disconnect()
|
||||||
|
sessionManager.endAllSessions("User disconnected")
|
||||||
|
webRTCManager?.stopAllStreaming()
|
||||||
|
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTED
|
||||||
|
_isStreaming.value = false
|
||||||
|
|
||||||
|
Logger.step("DISCONNECTED", "Disconnected from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновление URL сервера
|
||||||
|
*/
|
||||||
|
fun updateServerUrl(url: String) {
|
||||||
|
_serverUrl.value = url
|
||||||
|
context.getPreferences().edit {
|
||||||
|
putString("server_url", url)
|
||||||
|
}
|
||||||
|
Logger.step("SERVER_URL_UPDATED", "Server URL updated: $url")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистка запроса камеры
|
||||||
|
*/
|
||||||
|
fun clearCameraRequest() {
|
||||||
|
_cameraRequest.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка разрешений
|
||||||
|
*/
|
||||||
|
fun checkPermissions() {
|
||||||
|
permissionManager.checkPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Связывание с SocketService
|
||||||
|
*/
|
||||||
|
fun bindToSocketService(service: SocketService) {
|
||||||
|
Logger.step("VIEWMODEL_BIND_SERVICE", "Binding ViewModel to SocketService")
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за состоянием подключения
|
||||||
|
service.connectionState.collect { state ->
|
||||||
|
_connectionState.value = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за запросами камеры
|
||||||
|
service.cameraRequests.collect { request ->
|
||||||
|
_cameraRequest.value = request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
// Наблюдение за WebRTC событиями
|
||||||
|
service.webRTCEvents.collect { event ->
|
||||||
|
event?.let { handleWebRTCEvent(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback при получении разрешений
|
||||||
|
*/
|
||||||
|
fun onPermissionsGranted() {
|
||||||
|
Logger.step("VIEWMODEL_PERMISSIONS_GRANTED", "All permissions granted in ViewModel")
|
||||||
|
permissionManager.checkPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск тестового стриминга камеры
|
||||||
|
*/
|
||||||
|
fun startTestStreaming() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
Logger.step("START_TEST_STREAMING", "Starting test camera streaming")
|
||||||
|
|
||||||
|
// Создаем тестовую сессию
|
||||||
|
val testSessionId = "test_session_${System.currentTimeMillis()}"
|
||||||
|
val currentTime = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||||
|
.format(java.util.Date())
|
||||||
|
|
||||||
|
val testSessionInfo = SessionInfo(
|
||||||
|
sessionId = testSessionId,
|
||||||
|
deviceId = _deviceId.value,
|
||||||
|
operatorId = "test_operator",
|
||||||
|
cameraType = "back",
|
||||||
|
status = "streaming",
|
||||||
|
createdAt = currentTime
|
||||||
|
)
|
||||||
|
|
||||||
|
// Добавляем в активные сессии
|
||||||
|
val currentSessions = _activeSessions.value.toMutableMap()
|
||||||
|
currentSessions[testSessionId] = testSessionInfo
|
||||||
|
_activeSessions.value = currentSessions
|
||||||
|
|
||||||
|
_isStreaming.value = true
|
||||||
|
|
||||||
|
// Инициализируем WebRTC если нужно
|
||||||
|
if (webRTCManager == null) {
|
||||||
|
webRTCManager = WebRTCManager(context) { message ->
|
||||||
|
// Обработка сигналинга для тестового режима
|
||||||
|
Logger.d("Test signaling message: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем стриминг
|
||||||
|
webRTCManager?.startStreaming(testSessionId, "back")
|
||||||
|
|
||||||
|
Logger.step("START_TEST_STREAMING_SUCCESS", "Test streaming started successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("START_TEST_STREAMING_ERROR", "Failed to start test streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
Logger.step("VIEWMODEL_CLEARED", "MainViewModel cleared with ТЗ cleanup")
|
||||||
|
|
||||||
|
sessionManager.endAllSessions("App closed")
|
||||||
|
webRTCManager?.dispose()
|
||||||
|
camera2Manager.release()
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
364
app/src/main/java/com/example/godeye/SettingsScreen.kt
Normal file
364
app/src/main/java/com/example/godeye/SettingsScreen.kt
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
package com.example.godeye
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
import com.example.godeye.utils.getPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Экран настроек GodEye с расширенными параметрами согласно ТЗ
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onServerConfigSaved: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val prefs = context.getPreferences()
|
||||||
|
|
||||||
|
// Состояния настроек
|
||||||
|
var serverUrl by remember {
|
||||||
|
mutableStateOf(prefs.getString("server_url", "http://192.168.219.108:3001") ?: "")
|
||||||
|
}
|
||||||
|
var deviceName by remember {
|
||||||
|
mutableStateOf(prefs.getString("device_name", "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") ?: "")
|
||||||
|
}
|
||||||
|
var autoConnect by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("auto_connect", false))
|
||||||
|
}
|
||||||
|
var autoAcceptRequests by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("auto_accept_requests", true))
|
||||||
|
}
|
||||||
|
var enableNotifications by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("enable_notifications", true))
|
||||||
|
}
|
||||||
|
var keepScreenOn by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("keep_screen_on", false))
|
||||||
|
}
|
||||||
|
var preferredCamera by remember {
|
||||||
|
mutableStateOf(prefs.getString("preferred_camera", "back") ?: "back")
|
||||||
|
}
|
||||||
|
var streamQuality by remember {
|
||||||
|
mutableStateOf(prefs.getString("stream_quality", "720p") ?: "720p")
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.systemBarsPadding()
|
||||||
|
) {
|
||||||
|
// Шапка экрана
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Настройки GodEye",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackPressed) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Назад",
|
||||||
|
tint = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
// Сохраняем все настройки
|
||||||
|
prefs.edit {
|
||||||
|
putString("server_url", serverUrl)
|
||||||
|
putString("device_name", deviceName)
|
||||||
|
putBoolean("auto_connect", autoConnect)
|
||||||
|
putBoolean("auto_accept_requests", autoAcceptRequests)
|
||||||
|
putBoolean("enable_notifications", enableNotifications)
|
||||||
|
putBoolean("keep_screen_on", keepScreenOn)
|
||||||
|
putString("preferred_camera", preferredCamera)
|
||||||
|
putString("stream_quality", streamQuality)
|
||||||
|
}
|
||||||
|
onServerConfigSaved(serverUrl)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Сохранить",
|
||||||
|
color = GodEyeColors.SuccessGreen,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Секция "Сервер"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Подключение к серверу") {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = { serverUrl = it },
|
||||||
|
label = { Text("URL сервера") },
|
||||||
|
placeholder = { Text("http://192.168.1.100:3001") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
focusedTextColor = GodEyeColors.IvoryPure,
|
||||||
|
unfocusedTextColor = GodEyeColors.IvorySoft,
|
||||||
|
focusedLabelColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedLabelColor = GodEyeColors.IvorySoft
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Language,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Автоматическое подключение",
|
||||||
|
subtitle = "Подключаться к серверу при запуске приложения",
|
||||||
|
checked = autoConnect,
|
||||||
|
onCheckedChange = { autoConnect = it },
|
||||||
|
icon = Icons.Default.AutoAwesome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "Устройство"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Устройство") {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = deviceName,
|
||||||
|
onValueChange = { deviceName = it },
|
||||||
|
label = { Text("Имя устройства") },
|
||||||
|
placeholder = { Text("Android Device") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
focusedTextColor = GodEyeColors.IvoryPure,
|
||||||
|
unfocusedTextColor = GodEyeColors.IvorySoft,
|
||||||
|
focusedLabelColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedLabelColor = GodEyeColors.IvorySoft
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Smartphone,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Это имя будет отображаться операторам при подключении",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.padding(start = 48.dp, top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "Автоматизация"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Автоматизация") {
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Автоматическое принятие запросов",
|
||||||
|
subtitle = "Автоматически принимать запросы от операторов",
|
||||||
|
checked = autoAcceptRequests,
|
||||||
|
onCheckedChange = { autoAcceptRequests = it },
|
||||||
|
icon = Icons.Default.AutoAwesome
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Уведомления",
|
||||||
|
subtitle = "Показывать уведомления о входящих запросах",
|
||||||
|
checked = enableNotifications,
|
||||||
|
onCheckedChange = { enableNotifications = it },
|
||||||
|
icon = Icons.Default.Notifications
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Не выключать экран",
|
||||||
|
subtitle = "Экран остается включенным во время сессии",
|
||||||
|
checked = keepScreenOn,
|
||||||
|
onCheckedChange = { keepScreenOn = it },
|
||||||
|
icon = Icons.Default.ScreenLockPortrait
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "О приложении"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "О приложении") {
|
||||||
|
InfoCard(
|
||||||
|
title = "GodEye Android Client",
|
||||||
|
subtitle = "Версия 1.0.0 (Build 1)",
|
||||||
|
icon = Icons.Default.Info
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
InfoCard(
|
||||||
|
title = "Device ID",
|
||||||
|
subtitle = context.getPreferences().getString("device_id", "Неизвестно") ?: "Неизвестно",
|
||||||
|
icon = Icons.Default.Fingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsSection(
|
||||||
|
title: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GodEyeColors.IvoryPure,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsSwitchCard(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (checked) GodEyeColors.SuccessGreen else GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = GodEyeColors.IvoryPure,
|
||||||
|
checkedTrackColor = GodEyeColors.SuccessGreen,
|
||||||
|
uncheckedThumbColor = GodEyeColors.IvorySoft,
|
||||||
|
uncheckedTrackColor = GodEyeColors.NavyDark
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoCard(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
236
app/src/main/java/com/example/godeye/camera/CameraManager.kt
Normal file
236
app/src/main/java/com/example/godeye/camera/CameraManager.kt
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
package com.example.godeye.camera
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.camera.core.*
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CameraManager - безопасное управление камерой для предпросмотра
|
||||||
|
* Исправлены проблемы с освобождением ресурсов и утечками памяти
|
||||||
|
*/
|
||||||
|
class CameraManager(private val context: Context) {
|
||||||
|
|
||||||
|
private var cameraProvider: ProcessCameraProvider? = null
|
||||||
|
private var camera: Camera? = null
|
||||||
|
private var preview: Preview? = null
|
||||||
|
private var imageCapture: ImageCapture? = null
|
||||||
|
|
||||||
|
private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||||
|
private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
private var isReleased = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка разрешений камеры
|
||||||
|
*/
|
||||||
|
fun hasPermissions(): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED &&
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасная настройка камеры с предпросмотром
|
||||||
|
*/
|
||||||
|
fun setupCamera(
|
||||||
|
previewView: PreviewView,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
) {
|
||||||
|
if (isReleased) {
|
||||||
|
Logger.error("CAMERA_SETUP_ERROR", "Camera manager already released")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сначала безопасно освобождаем предыдущие ресурсы
|
||||||
|
safeCameraCleanup()
|
||||||
|
|
||||||
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
|
cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
currentCameraSelector = cameraSelector
|
||||||
|
|
||||||
|
// Создание preview use case с безопасными настройками
|
||||||
|
preview = Preview.Builder()
|
||||||
|
.setTargetRotation(previewView.display.rotation)
|
||||||
|
.build().also {
|
||||||
|
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание image capture use case с оптимизацией для Android 9
|
||||||
|
imageCapture = ImageCapture.Builder()
|
||||||
|
.setTargetRotation(previewView.display.rotation)
|
||||||
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Отвязка всех use cases перед привязкой новых
|
||||||
|
cameraProvider?.unbindAll()
|
||||||
|
|
||||||
|
// Безопасная привязка use cases к lifecycle
|
||||||
|
camera = cameraProvider?.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
cameraSelector,
|
||||||
|
preview,
|
||||||
|
imageCapture
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.step("CAMERA_SETUP", "Camera setup completed safely")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_SETUP_ERROR", "Failed to setup camera safely", e)
|
||||||
|
safeCameraCleanup()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасная очистка ресурсов камеры
|
||||||
|
*/
|
||||||
|
private fun safeCameraCleanup() {
|
||||||
|
try {
|
||||||
|
cameraProvider?.unbindAll()
|
||||||
|
preview = null
|
||||||
|
imageCapture = null
|
||||||
|
camera = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_CLEANUP_ERROR", "Error during camera cleanup", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение камеры с безопасной обработкой
|
||||||
|
*/
|
||||||
|
fun switchCamera(): CameraSelector {
|
||||||
|
if (isReleased) return currentCameraSelector
|
||||||
|
|
||||||
|
currentCameraSelector = if (currentCameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
|
||||||
|
CameraSelector.DEFAULT_FRONT_CAMERA
|
||||||
|
} else {
|
||||||
|
CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
}
|
||||||
|
return currentCameraSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасное включение/выключение вспышки
|
||||||
|
*/
|
||||||
|
fun toggleFlash() {
|
||||||
|
if (isReleased) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
camera?.let { camera ->
|
||||||
|
if (camera.cameraInfo.hasFlashUnit()) {
|
||||||
|
val flashMode = camera.cameraInfo.torchState.value
|
||||||
|
camera.cameraControl.enableTorch(flashMode == TorchState.OFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("FLASH_TOGGLE_ERROR", "Error toggling flash", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасная съемка фото
|
||||||
|
*/
|
||||||
|
fun takePhoto(
|
||||||
|
outputFile: File,
|
||||||
|
onPhotoTaken: (File) -> Unit,
|
||||||
|
onError: (Exception) -> Unit
|
||||||
|
) {
|
||||||
|
if (isReleased) {
|
||||||
|
onError(Exception("Camera manager released"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageCapture = imageCapture ?: run {
|
||||||
|
onError(Exception("ImageCapture not initialized"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputFile).build()
|
||||||
|
|
||||||
|
imageCapture.takePicture(
|
||||||
|
outputOptions,
|
||||||
|
ContextCompat.getMainExecutor(context),
|
||||||
|
object : ImageCapture.OnImageSavedCallback {
|
||||||
|
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
||||||
|
onPhotoTaken(outputFile)
|
||||||
|
Logger.step("PHOTO_TAKEN", "Photo saved safely to: ${outputFile.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(exception: ImageCaptureException) {
|
||||||
|
onError(exception)
|
||||||
|
Logger.error("PHOTO_ERROR", "Photo capture failed safely", exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError(e)
|
||||||
|
Logger.error("PHOTO_SETUP_ERROR", "Failed to setup photo capture", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начало записи видео - заглушка для совместимости
|
||||||
|
*/
|
||||||
|
fun startRecording(
|
||||||
|
outputFile: File,
|
||||||
|
onRecordingStarted: () -> Unit,
|
||||||
|
onError: (Exception) -> Unit
|
||||||
|
) {
|
||||||
|
onError(Exception("Video recording not supported on this device for stability"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановка записи видео - заглушка для совместимости
|
||||||
|
*/
|
||||||
|
fun stopRecording() {
|
||||||
|
Logger.step("VIDEO_RECORDING_STOPPED", "Video recording stop requested (not supported)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасное освобождение ресурсов
|
||||||
|
*/
|
||||||
|
fun release() {
|
||||||
|
if (isReleased) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
Logger.step("CAMERA_MANAGER_RELEASING", "Starting safe camera manager release")
|
||||||
|
|
||||||
|
isReleased = true
|
||||||
|
|
||||||
|
// Безопасная очистка камеры
|
||||||
|
safeCameraCleanup()
|
||||||
|
|
||||||
|
// Безопасное завершение executor
|
||||||
|
cameraExecutor.shutdown()
|
||||||
|
try {
|
||||||
|
if (!cameraExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||||
|
cameraExecutor.shutdownNow()
|
||||||
|
}
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
cameraExecutor.shutdownNow()
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraProvider = null
|
||||||
|
|
||||||
|
Logger.step("CAMERA_MANAGER_RELEASED", "Camera manager resources released safely")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_RELEASE_ERROR", "Error during camera manager release", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
315
app/src/main/java/com/example/godeye/camera/CameraScreen.kt
Normal file
315
app/src/main/java/com/example/godeye/camera/CameraScreen.kt
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
package com.example.godeye.camera
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.SurfaceView
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.camera.core.*
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.example.godeye.ui.components.*
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CameraScreen(
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
sessionId: String = "",
|
||||||
|
@Suppress("UNUSED_PARAMETER") operatorId: String = ""
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
var hasPermissions by remember { mutableStateOf(false) }
|
||||||
|
var showError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// Упрощенный CameraManager без сложных анимаций
|
||||||
|
val cameraManager = remember {
|
||||||
|
try {
|
||||||
|
CameraManager(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_MANAGER_CREATE_ERROR", "Failed to create camera manager", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val previewView = remember {
|
||||||
|
try {
|
||||||
|
PreviewView(context).apply {
|
||||||
|
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||||
|
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("PREVIEW_VIEW_CREATE_ERROR", "Failed to create preview view", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка разрешений
|
||||||
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
hasPermissions = permissions.values.all { it }
|
||||||
|
if (!hasPermissions) {
|
||||||
|
showError = "Необходимы разрешения для работы с камерой"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
hasPermissions = cameraManager?.hasPermissions() ?: false
|
||||||
|
if (!hasPermissions) {
|
||||||
|
permissionLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простая инициализация камеры БЕЗ сложных анимаций
|
||||||
|
LaunchedEffect(hasPermissions) {
|
||||||
|
if (hasPermissions && cameraManager != null && previewView != null) {
|
||||||
|
try {
|
||||||
|
Logger.step("CAMERA_INIT_START", "Starting simple camera initialization")
|
||||||
|
cameraManager.setupCamera(previewView, lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA)
|
||||||
|
Logger.step("CAMERA_INIT_SUCCESS", "Simple camera initialized successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError = "Ошибка инициализации камеры: ${e.message}"
|
||||||
|
Logger.error("CAMERA_INIT_ERROR", "Camera initialization failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Безопасное освобождение ресурсов при закрытии
|
||||||
|
DisposableEffect(cameraManager) {
|
||||||
|
onDispose {
|
||||||
|
try {
|
||||||
|
Logger.step("CAMERA_SCREEN_DISPOSE", "Disposing camera screen safely")
|
||||||
|
cameraManager?.release()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_DISPOSE_ERROR", "Error disposing camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// УПРОЩЕННЫЙ UI БЕЗ СЛОЖНЫХ АНИМАЦИЙ
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
) {
|
||||||
|
if (hasPermissions && previewView != null && cameraManager != null) {
|
||||||
|
// Простой preview БЕЗ сложных эффектов
|
||||||
|
AndroidView(
|
||||||
|
factory = { previewView },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Простая верхняя панель БЕЗ анимаций
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color.Black.copy(alpha = 0.7f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onBackPressed,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Gray
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("← Назад", color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "GodEye Camera",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(60.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простая нижняя панель БЕЗ анимаций
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color.Black.copy(alpha = 0.7f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Простая кнопка фото БЕЗ анимаций
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
try {
|
||||||
|
val photoFile = File(
|
||||||
|
context.externalCacheDir,
|
||||||
|
"photo_${System.currentTimeMillis()}.jpg"
|
||||||
|
)
|
||||||
|
cameraManager.takePhoto(
|
||||||
|
photoFile,
|
||||||
|
onPhotoTaken = {
|
||||||
|
Logger.step("PHOTO_TAKEN", "Photo taken successfully")
|
||||||
|
},
|
||||||
|
onError = { error ->
|
||||||
|
showError = "Ошибка съемки: ${error.message}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError = "Ошибка съемки: ${e.message}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Blue
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("📷 Фото", color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простая кнопка переключения камеры БЕЗ анимаций
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
try {
|
||||||
|
val newCameraSelector = cameraManager.switchCamera()
|
||||||
|
// Простое пересоздание камеры с новым селектором
|
||||||
|
cameraManager.setupCamera(previewView, lifecycleOwner, newCameraSelector)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError = "Ошибка переключения камеры"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Green
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("🔄 Камера", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Простой экран разрешений БЕЗ анимаций
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (cameraManager == null || previewView == null) "Ошибка камеры" else "Требуются разрешения",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = Color.White,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
if (cameraManager != null && previewView != null) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
permissionLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Blue
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Предоставить разрешения", color = Color.White)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = onBackPressed,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.Red
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Вернуться назад", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Простое отображение ошибок БЕЗ анимаций
|
||||||
|
showError?.let { error ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = Color.Red.copy(alpha = 0.9f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { showError = null },
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text("Закрыть", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
Normal file
303
app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package com.example.godeye.managers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.SurfaceTexture
|
||||||
|
import android.hardware.camera2.*
|
||||||
|
import android.util.Size
|
||||||
|
import android.view.Surface
|
||||||
|
import com.example.godeye.models.AppError
|
||||||
|
import com.example.godeye.models.CameraInfo
|
||||||
|
import com.example.godeye.models.CameraState
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import java.util.concurrent.Semaphore
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Camera2Manager - управление камерами устройства с использованием Camera2 API
|
||||||
|
* Соответствует требованиям ТЗ для работы с различными типами камер
|
||||||
|
*/
|
||||||
|
class Camera2Manager(private val context: Context) {
|
||||||
|
|
||||||
|
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
private var captureSession: CameraCaptureSession? = null
|
||||||
|
private var currentCameraId: String? = null
|
||||||
|
private val cameraOpenCloseLock = Semaphore(1)
|
||||||
|
|
||||||
|
private val _cameraState = MutableStateFlow(CameraState.CLOSED)
|
||||||
|
val cameraState: StateFlow<CameraState> = _cameraState.asStateFlow()
|
||||||
|
|
||||||
|
private val _availableCameras = MutableStateFlow<List<CameraInfo>>(emptyList())
|
||||||
|
val availableCameras: StateFlow<List<CameraInfo>> = _availableCameras.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
detectAvailableCameras()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение доступных камер устройства согласно ТЗ
|
||||||
|
* Поддерживает: back, front, wide, telephoto
|
||||||
|
*/
|
||||||
|
private fun detectAvailableCameras() {
|
||||||
|
Logger.step("CAMERA_DETECTION", "Detecting available cameras")
|
||||||
|
|
||||||
|
val cameras = mutableListOf<CameraInfo>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (cameraId in cameraManager.cameraIdList) {
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
||||||
|
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||||
|
val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
||||||
|
|
||||||
|
val cameraType = when (facing) {
|
||||||
|
CameraCharacteristics.LENS_FACING_BACK -> {
|
||||||
|
// Определяем тип задней камеры по фокусному расстоянию
|
||||||
|
when {
|
||||||
|
focalLengths != null && focalLengths.size > 1 -> {
|
||||||
|
if (focalLengths.minOrNull()!! < 3.0f) "ultra_wide"
|
||||||
|
else if (focalLengths.maxOrNull()!! > 6.0f) "telephoto"
|
||||||
|
else "back"
|
||||||
|
}
|
||||||
|
else -> "back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CameraCharacteristics.LENS_FACING_FRONT -> "front"
|
||||||
|
else -> "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraType != "unknown") {
|
||||||
|
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
||||||
|
val sizes = configMap?.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
|
||||||
|
|
||||||
|
cameras.add(
|
||||||
|
CameraInfo(
|
||||||
|
id = cameraId,
|
||||||
|
type = cameraType,
|
||||||
|
facing = facing ?: -1,
|
||||||
|
supportedSizes = sizes.toList(),
|
||||||
|
focalLengths = focalLengths?.toList() ?: emptyList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.d("Camera detected: $cameraId, type: $cameraType, sizes: ${sizes.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_availableCameras.value = cameras
|
||||||
|
Logger.step("CAMERA_DETECTION_COMPLETE", "Found ${cameras.size} cameras: ${cameras.map { it.type }}")
|
||||||
|
|
||||||
|
} catch (e: CameraAccessException) {
|
||||||
|
Logger.error("CAMERA_DETECTION_ERROR", "Failed to detect cameras", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка доступных типов камер для регистрации на сервере
|
||||||
|
*/
|
||||||
|
fun getAvailableCameraTypes(): List<String> {
|
||||||
|
return _availableCameras.value.map { it.type }.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск камеры указанного типа
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun startCamera(cameraType: String, surface: Surface, onError: (AppError) -> Unit) {
|
||||||
|
Logger.step("CAMERA_START", "Starting camera: $cameraType")
|
||||||
|
|
||||||
|
val cameraInfo = _availableCameras.value.find { it.type == cameraType }
|
||||||
|
if (cameraInfo == null) {
|
||||||
|
Logger.error("CAMERA_NOT_FOUND", "Camera type not available: $cameraType", null)
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
|
||||||
|
Logger.error("CAMERA_LOCK_TIMEOUT", "Camera lock timeout", null)
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_cameraState.value = CameraState.OPENING
|
||||||
|
|
||||||
|
val stateCallback = object : CameraDevice.StateCallback() {
|
||||||
|
override fun onOpened(camera: CameraDevice) {
|
||||||
|
Logger.step("CAMERA_OPENED", "Camera opened: ${camera.id}")
|
||||||
|
cameraOpenCloseLock.release()
|
||||||
|
cameraDevice = camera
|
||||||
|
currentCameraId = camera.id
|
||||||
|
_cameraState.value = CameraState.OPENED
|
||||||
|
createCaptureSession(surface, onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(camera: CameraDevice) {
|
||||||
|
Logger.step("CAMERA_DISCONNECTED", "Camera disconnected: ${camera.id}")
|
||||||
|
cameraOpenCloseLock.release()
|
||||||
|
camera.close()
|
||||||
|
cameraDevice = null
|
||||||
|
currentCameraId = null
|
||||||
|
_cameraState.value = CameraState.CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(camera: CameraDevice, error: Int) {
|
||||||
|
Logger.error("CAMERA_ERROR", "Camera error: $error for ${camera.id}", null)
|
||||||
|
cameraOpenCloseLock.release()
|
||||||
|
camera.close()
|
||||||
|
cameraDevice = null
|
||||||
|
currentCameraId = null
|
||||||
|
_cameraState.value = CameraState.ERROR
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraManager.openCamera(cameraInfo.id, stateCallback, null)
|
||||||
|
|
||||||
|
} catch (e: CameraAccessException) {
|
||||||
|
Logger.error("CAMERA_START_ERROR", "Failed to start camera", e)
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Logger.error("CAMERA_PERMISSION_ERROR", "Camera permission denied", e)
|
||||||
|
onError(AppError.CameraPermissionDenied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание сессии захвата для передачи видео
|
||||||
|
*/
|
||||||
|
private fun createCaptureSession(surface: Surface, onError: (AppError) -> Unit) {
|
||||||
|
try {
|
||||||
|
val camera = cameraDevice ?: run {
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_cameraState.value = CameraState.CONFIGURING
|
||||||
|
|
||||||
|
val sessionCallback = object : CameraCaptureSession.StateCallback() {
|
||||||
|
override fun onConfigured(session: CameraCaptureSession) {
|
||||||
|
Logger.step("CAPTURE_SESSION_CONFIGURED", "Capture session configured")
|
||||||
|
captureSession = session
|
||||||
|
_cameraState.value = CameraState.ACTIVE
|
||||||
|
startPreview(session, surface, onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigureFailed(session: CameraCaptureSession) {
|
||||||
|
Logger.error("CAPTURE_SESSION_FAILED", "Failed to configure capture session", null)
|
||||||
|
_cameraState.value = CameraState.ERROR
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.createCaptureSession(listOf(surface), sessionCallback, null)
|
||||||
|
|
||||||
|
} catch (e: CameraAccessException) {
|
||||||
|
Logger.error("CAPTURE_SESSION_ERROR", "Failed to create capture session", e)
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск предварительного просмотра
|
||||||
|
*/
|
||||||
|
private fun startPreview(session: CameraCaptureSession, surface: Surface, onError: (AppError) -> Unit) {
|
||||||
|
try {
|
||||||
|
val camera = cameraDevice ?: run {
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val previewRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
||||||
|
previewRequestBuilder.addTarget(surface)
|
||||||
|
|
||||||
|
// Настройки для оптимального качества видео
|
||||||
|
previewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
||||||
|
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
|
||||||
|
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
|
||||||
|
|
||||||
|
val previewRequest = previewRequestBuilder.build()
|
||||||
|
|
||||||
|
session.setRepeatingRequest(previewRequest, object : CameraCaptureSession.CaptureCallback() {
|
||||||
|
override fun onCaptureStarted(
|
||||||
|
session: CameraCaptureSession,
|
||||||
|
request: CaptureRequest,
|
||||||
|
timestamp: Long,
|
||||||
|
frameNumber: Long
|
||||||
|
) {
|
||||||
|
// Preview started
|
||||||
|
}
|
||||||
|
}, null)
|
||||||
|
|
||||||
|
Logger.step("CAMERA_PREVIEW_STARTED", "Camera preview started")
|
||||||
|
|
||||||
|
} catch (e: CameraAccessException) {
|
||||||
|
Logger.error("CAMERA_PREVIEW_ERROR", "Failed to start preview", e)
|
||||||
|
onError(AppError.CameraNotAvailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение на другую камеру
|
||||||
|
*/
|
||||||
|
fun switchCamera(newCameraType: String, surface: Surface, onError: (AppError) -> Unit) {
|
||||||
|
Logger.step("CAMERA_SWITCH", "Switching camera to: $newCameraType")
|
||||||
|
|
||||||
|
stopCamera()
|
||||||
|
startCamera(newCameraType, surface, onError)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановка камеры
|
||||||
|
*/
|
||||||
|
fun stopCamera() {
|
||||||
|
Logger.step("CAMERA_STOP", "Stopping camera")
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraOpenCloseLock.acquire()
|
||||||
|
|
||||||
|
captureSession?.close()
|
||||||
|
captureSession = null
|
||||||
|
|
||||||
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
currentCameraId = null
|
||||||
|
|
||||||
|
_cameraState.value = CameraState.CLOSED
|
||||||
|
|
||||||
|
Logger.step("CAMERA_STOPPED", "Camera stopped")
|
||||||
|
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Logger.error("CAMERA_STOP_ERROR", "Interrupted while stopping camera", e)
|
||||||
|
} finally {
|
||||||
|
cameraOpenCloseLock.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение оптимального размера для WebRTC
|
||||||
|
*/
|
||||||
|
fun getOptimalSize(cameraType: String, maxWidth: Int = 1920, maxHeight: Int = 1080): Size? {
|
||||||
|
val cameraInfo = _availableCameras.value.find { it.type == cameraType } ?: return null
|
||||||
|
|
||||||
|
return cameraInfo.supportedSizes
|
||||||
|
.filter { it.width <= maxWidth && it.height <= maxHeight }
|
||||||
|
.maxByOrNull { it.width * it.height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение текущего состояния камеры
|
||||||
|
*/
|
||||||
|
fun getCurrentCameraType(): String? {
|
||||||
|
val cameraId = currentCameraId ?: return null
|
||||||
|
return _availableCameras.value.find { it.id == cameraId }?.type
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
Logger.step("CAMERA_MANAGER_RELEASE", "Releasing Camera2Manager")
|
||||||
|
stopCamera()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
package com.example.godeye.managers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.hardware.camera2.*
|
|
||||||
import android.media.MediaRecorder
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import android.util.Size
|
|
||||||
import android.view.Surface
|
|
||||||
import com.example.godeye.models.AppError
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import com.example.godeye.utils.Logger
|
|
||||||
import com.example.godeye.utils.getCameraIdForType
|
|
||||||
import com.example.godeye.utils.getAvailableCameraTypes
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Менеджер для управления камерами устройства
|
|
||||||
*/
|
|
||||||
class CameraManager(private val context: Context) {
|
|
||||||
|
|
||||||
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
|
|
||||||
private var currentCameraId: String? = null
|
|
||||||
private var captureSession: CameraCaptureSession? = null
|
|
||||||
private var cameraDevice: CameraDevice? = null
|
|
||||||
private var backgroundThread: HandlerThread? = null
|
|
||||||
private var backgroundHandler: Handler? = null
|
|
||||||
|
|
||||||
private val _isRecording = MutableStateFlow(false)
|
|
||||||
val isRecording: StateFlow<Boolean> = _isRecording.asStateFlow()
|
|
||||||
|
|
||||||
private val _currentCameraType = MutableStateFlow<String?>(null)
|
|
||||||
val currentCameraType: StateFlow<String?> = _currentCameraType.asStateFlow()
|
|
||||||
|
|
||||||
private val _error = MutableStateFlow<AppError?>(null)
|
|
||||||
val error: StateFlow<AppError?> = _error.asStateFlow()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация фонового потока для камеры
|
|
||||||
*/
|
|
||||||
private fun startBackgroundThread() {
|
|
||||||
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
|
|
||||||
backgroundHandler = Handler(backgroundThread?.looper!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Остановка фонового потока
|
|
||||||
*/
|
|
||||||
private fun stopBackgroundThread() {
|
|
||||||
backgroundThread?.quitSafely()
|
|
||||||
try {
|
|
||||||
backgroundThread?.join()
|
|
||||||
backgroundThread = null
|
|
||||||
backgroundHandler = null
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
Logger.e("Error stopping background thread", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить список доступных типов камер
|
|
||||||
*/
|
|
||||||
fun getAvailableCameraTypes(): List<String> {
|
|
||||||
return cameraManager.getAvailableCameraTypes()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Открыть камеру указанного типа
|
|
||||||
*/
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun openCamera(cameraType: String, surface: Surface, onSuccess: () -> Unit = {}, onError: (AppError) -> Unit = {}) {
|
|
||||||
try {
|
|
||||||
val cameraId = cameraManager.getCameraIdForType(cameraType)
|
|
||||||
if (cameraId == null) {
|
|
||||||
val error = AppError.CameraError("Camera type $cameraType not available")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
startBackgroundThread()
|
|
||||||
|
|
||||||
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
|
||||||
override fun onOpened(camera: CameraDevice) {
|
|
||||||
Logger.d("Camera opened: $cameraId")
|
|
||||||
cameraDevice = camera
|
|
||||||
currentCameraId = cameraId
|
|
||||||
_currentCameraType.value = cameraType
|
|
||||||
createCameraPreviewSession(surface, onSuccess, onError)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisconnected(camera: CameraDevice) {
|
|
||||||
Logger.d("Camera disconnected: $cameraId")
|
|
||||||
camera.close()
|
|
||||||
cameraDevice = null
|
|
||||||
currentCameraId = null
|
|
||||||
_currentCameraType.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(camera: CameraDevice, error: Int) {
|
|
||||||
Logger.e("Camera error: $error")
|
|
||||||
camera.close()
|
|
||||||
cameraDevice = null
|
|
||||||
currentCameraId = null
|
|
||||||
_currentCameraType.value = null
|
|
||||||
val appError = AppError.CameraError("Camera error: $error")
|
|
||||||
_error.value = appError
|
|
||||||
onError(appError)
|
|
||||||
}
|
|
||||||
}, backgroundHandler)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error opening camera", e)
|
|
||||||
val error = AppError.CameraError("Failed to open camera: ${e.message}")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать сессию предварительного просмотра камеры
|
|
||||||
*/
|
|
||||||
private fun createCameraPreviewSession(surface: Surface, onSuccess: () -> Unit, onError: (AppError) -> Unit) {
|
|
||||||
try {
|
|
||||||
val cameraDevice = this.cameraDevice ?: run {
|
|
||||||
val error = AppError.CameraError("Camera device is null")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
|
||||||
captureRequestBuilder.addTarget(surface)
|
|
||||||
|
|
||||||
// Используем совместимый подход для всех версий Android
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
cameraDevice.createCaptureSession(
|
|
||||||
listOf(surface),
|
|
||||||
object : CameraCaptureSession.StateCallback() {
|
|
||||||
override fun onConfigured(session: CameraCaptureSession) {
|
|
||||||
captureSession = session
|
|
||||||
try {
|
|
||||||
captureRequestBuilder.set(
|
|
||||||
CaptureRequest.CONTROL_AF_MODE,
|
|
||||||
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|
|
||||||
)
|
|
||||||
|
|
||||||
val captureRequest = captureRequestBuilder.build()
|
|
||||||
session.setRepeatingRequest(captureRequest, null, backgroundHandler)
|
|
||||||
_isRecording.value = true
|
|
||||||
Logger.d("Camera preview session created successfully")
|
|
||||||
onSuccess()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error starting camera preview", e)
|
|
||||||
val error = AppError.CameraError("Failed to start preview: ${e.message}")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigureFailed(session: CameraCaptureSession) {
|
|
||||||
Logger.e("Camera capture session configuration failed")
|
|
||||||
val error = AppError.CameraError("Failed to configure capture session")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
backgroundHandler
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error creating camera preview session", e)
|
|
||||||
val error = AppError.CameraError("Failed to create preview session: ${e.message}")
|
|
||||||
_error.value = error
|
|
||||||
onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Переключить на другой тип камеры
|
|
||||||
*/
|
|
||||||
fun switchCamera(newCameraType: String, surface: Surface, onSuccess: () -> Unit = {}, onError: (AppError) -> Unit = {}) {
|
|
||||||
Logger.d("Switching camera from ${_currentCameraType.value} to $newCameraType")
|
|
||||||
closeCamera()
|
|
||||||
openCamera(newCameraType, surface, onSuccess, onError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить оптимальный размер для предварительного просмотра
|
|
||||||
*/
|
|
||||||
fun getOptimalPreviewSize(cameraType: String): Size? {
|
|
||||||
return try {
|
|
||||||
val cameraId = cameraManager.getCameraIdForType(cameraType) ?: return null
|
|
||||||
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
|
|
||||||
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
|
||||||
val outputSizes = map?.getOutputSizes(SurfaceTexture::class.java)
|
|
||||||
|
|
||||||
// Выбираем размер близкий к 1080p, но не превышающий его
|
|
||||||
outputSizes?.find { it.width <= 1920 && it.height <= 1080 }
|
|
||||||
?: outputSizes?.minByOrNull { it.width * it.height }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error getting optimal preview size", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Закрыть текущую камеру
|
|
||||||
*/
|
|
||||||
fun closeCamera() {
|
|
||||||
try {
|
|
||||||
captureSession?.close()
|
|
||||||
captureSession = null
|
|
||||||
|
|
||||||
cameraDevice?.close()
|
|
||||||
cameraDevice = null
|
|
||||||
|
|
||||||
currentCameraId = null
|
|
||||||
_currentCameraType.value = null
|
|
||||||
_isRecording.value = false
|
|
||||||
|
|
||||||
stopBackgroundThread()
|
|
||||||
Logger.d("Camera closed successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error closing camera", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить, открыта ли камера
|
|
||||||
*/
|
|
||||||
fun isCameraOpen(): Boolean {
|
|
||||||
return cameraDevice != null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить ошибку
|
|
||||||
*/
|
|
||||||
fun clearError() {
|
|
||||||
_error.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,99 +4,218 @@ import android.Manifest
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.example.godeye.models.AppError
|
||||||
import com.example.godeye.utils.Logger
|
import com.example.godeye.utils.Logger
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Менеджер для управления разрешениями приложения
|
* PermissionManager - управление разрешениями приложения
|
||||||
|
* Соответствует требованиям ТЗ для работы с CAMERA, RECORD_AUDIO, INTERNET
|
||||||
*/
|
*/
|
||||||
class PermissionManager(private val context: Context) {
|
class PermissionManager(private val context: Context) {
|
||||||
|
|
||||||
|
private val _permissionsGranted = MutableStateFlow(false)
|
||||||
|
val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow()
|
||||||
|
|
||||||
|
private val _missingPermissions = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val missingPermissions: StateFlow<List<String>> = _missingPermissions.asStateFlow()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Все необходимые разрешения согласно ТЗ
|
||||||
|
*/
|
||||||
val REQUIRED_PERMISSIONS = arrayOf(
|
val REQUIRED_PERMISSIONS = arrayOf(
|
||||||
Manifest.permission.CAMERA,
|
Manifest.permission.CAMERA,
|
||||||
Manifest.permission.RECORD_AUDIO,
|
Manifest.permission.RECORD_AUDIO,
|
||||||
Manifest.permission.INTERNET,
|
Manifest.permission.INTERNET,
|
||||||
Manifest.permission.ACCESS_NETWORK_STATE,
|
Manifest.permission.ACCESS_NETWORK_STATE,
|
||||||
Manifest.permission.WAKE_LOCK,
|
Manifest.permission.WAKE_LOCK,
|
||||||
Manifest.permission.FOREGROUND_SERVICE,
|
Manifest.permission.FOREGROUND_SERVICE
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
).apply {
|
||||||
)
|
// Добавляем FOREGROUND_SERVICE_CAMERA для API 34+
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
val CAMERA_PERMISSIONS = arrayOf(
|
plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
|
||||||
Manifest.permission.CAMERA,
|
}
|
||||||
Manifest.permission.RECORD_AUDIO
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить, есть ли все необходимые разрешения
|
|
||||||
*/
|
|
||||||
fun hasAllRequiredPermissions(): Boolean {
|
|
||||||
return REQUIRED_PERMISSIONS.all { permission ->
|
|
||||||
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить разрешения для камеры
|
* Критически важные разрешения для основной функциональности
|
||||||
*/
|
*/
|
||||||
fun hasCameraPermissions(): Boolean {
|
val CRITICAL_PERMISSIONS = arrayOf(
|
||||||
return CAMERA_PERMISSIONS.all { permission ->
|
|
||||||
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить конкретное разрешение
|
|
||||||
*/
|
|
||||||
fun hasPermission(permission: String): Boolean {
|
|
||||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить список отсутствующих разрешений
|
|
||||||
*/
|
|
||||||
fun getMissingPermissions(): List<String> {
|
|
||||||
return REQUIRED_PERMISSIONS.filter { permission ->
|
|
||||||
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить список отсутствующих разрешений для камеры
|
|
||||||
*/
|
|
||||||
fun getMissingCameraPermissions(): List<String> {
|
|
||||||
return CAMERA_PERMISSIONS.filter { permission ->
|
|
||||||
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить критические разрешения для основной функциональности
|
|
||||||
*/
|
|
||||||
fun hasCriticalPermissions(): Boolean {
|
|
||||||
val criticalPermissions = arrayOf(
|
|
||||||
Manifest.permission.CAMERA,
|
Manifest.permission.CAMERA,
|
||||||
Manifest.permission.RECORD_AUDIO,
|
Manifest.permission.RECORD_AUDIO,
|
||||||
Manifest.permission.INTERNET
|
Manifest.permission.INTERNET
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return criticalPermissions.all { permission ->
|
/**
|
||||||
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
* Проверка всех необходимых разрешений
|
||||||
|
*/
|
||||||
|
fun checkPermissions(): Boolean {
|
||||||
|
Logger.step("PERMISSION_CHECK", "Checking all required permissions")
|
||||||
|
|
||||||
|
val missing = mutableListOf<String>()
|
||||||
|
|
||||||
|
REQUIRED_PERMISSIONS.forEach { permission ->
|
||||||
|
if (!isPermissionGranted(permission)) {
|
||||||
|
missing.add(permission)
|
||||||
|
Logger.d("Missing permission: $permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_missingPermissions.value = missing
|
||||||
|
val allGranted = missing.isEmpty()
|
||||||
|
_permissionsGranted.value = allGranted
|
||||||
|
|
||||||
|
Logger.step("PERMISSION_CHECK_RESULT",
|
||||||
|
if (allGranted) "All permissions granted"
|
||||||
|
else "Missing ${missing.size} permissions: ${missing.joinToString()}")
|
||||||
|
|
||||||
|
return allGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка критически важных разрешений
|
||||||
|
*/
|
||||||
|
fun checkCriticalPermissions(): Boolean {
|
||||||
|
val missing = CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }
|
||||||
|
|
||||||
|
if (missing.isNotEmpty()) {
|
||||||
|
Logger.step("CRITICAL_PERMISSIONS_MISSING",
|
||||||
|
"Missing critical permissions: ${missing.joinToString()}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("CRITICAL_PERMISSIONS_OK", "All critical permissions granted")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка отдельного разрешения
|
||||||
|
*/
|
||||||
|
fun isPermissionGranted(permission: String): Boolean {
|
||||||
|
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка разрешения камеры
|
||||||
|
*/
|
||||||
|
fun hasCameraPermission(): Boolean {
|
||||||
|
return isPermissionGranted(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка разрешения микрофона
|
||||||
|
*/
|
||||||
|
fun hasAudioPermission(): Boolean {
|
||||||
|
return isPermissionGranted(Manifest.permission.RECORD_AUDIO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка разрешений для WebRTC
|
||||||
|
*/
|
||||||
|
fun hasWebRTCPermissions(): Boolean {
|
||||||
|
return hasCameraPermission() && hasAudioPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка отсутствующих разрешений для запроса
|
||||||
|
*/
|
||||||
|
fun getMissingPermissions(): Array<String> {
|
||||||
|
return REQUIRED_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка критически важных отсутствующих разрешений
|
||||||
|
*/
|
||||||
|
fun getMissingCriticalPermissions(): Array<String> {
|
||||||
|
return CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка результата запроса разрешений
|
||||||
|
*/
|
||||||
|
fun onPermissionsResult(
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
): PermissionResult {
|
||||||
|
Logger.step("PERMISSION_RESULT", "Processing permission request result")
|
||||||
|
|
||||||
|
val granted = mutableListOf<String>()
|
||||||
|
val denied = mutableListOf<String>()
|
||||||
|
|
||||||
|
permissions.forEachIndexed { index, permission ->
|
||||||
|
if (grantResults[index] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
granted.add(permission)
|
||||||
|
Logger.d("Permission granted: $permission")
|
||||||
|
} else {
|
||||||
|
denied.add(permission)
|
||||||
|
Logger.d("Permission denied: $permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем состояние
|
||||||
|
checkPermissions()
|
||||||
|
|
||||||
|
val result = when {
|
||||||
|
denied.isEmpty() -> PermissionResult.AllGranted
|
||||||
|
denied.any { it in CRITICAL_PERMISSIONS } -> PermissionResult.CriticalDenied(denied)
|
||||||
|
else -> PermissionResult.SomeGranted(granted, denied)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("PERMISSION_RESULT_PROCESSED",
|
||||||
|
"Result: ${result::class.simpleName}, granted: ${granted.size}, denied: ${denied.size}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение ошибки для отсутствующих разрешений
|
||||||
|
*/
|
||||||
|
fun getPermissionError(): AppError? {
|
||||||
|
return when {
|
||||||
|
!hasCameraPermission() -> AppError.CameraPermissionDenied
|
||||||
|
!hasAudioPermission() -> AppError.CameraPermissionDenied // Аудио тоже критично
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Логирование состояния разрешений
|
* Получение человекочитаемого описания разрешения
|
||||||
*/
|
*/
|
||||||
fun logPermissionsStatus() {
|
fun getPermissionDescription(permission: String): String {
|
||||||
Logger.d("=== Permission Status ===")
|
return when (permission) {
|
||||||
REQUIRED_PERMISSIONS.forEach { permission ->
|
Manifest.permission.CAMERA -> "Доступ к камере для видеосвязи"
|
||||||
val granted = hasPermission(permission)
|
Manifest.permission.RECORD_AUDIO -> "Доступ к микрофону для аудиосвязи"
|
||||||
Logger.d("$permission: ${if (granted) "GRANTED" else "DENIED"}")
|
Manifest.permission.INTERNET -> "Доступ к интернету для подключения к серверу"
|
||||||
|
Manifest.permission.ACCESS_NETWORK_STATE -> "Проверка состояния сети"
|
||||||
|
Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства"
|
||||||
|
Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме"
|
||||||
|
Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой"
|
||||||
|
else -> "Системное разрешение"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка необходимости объяснения разрешения
|
||||||
|
*/
|
||||||
|
fun shouldShowRationale(permission: String): Boolean {
|
||||||
|
// Для системных разрешений обычно не показываем rationale
|
||||||
|
return when (permission) {
|
||||||
|
Manifest.permission.CAMERA,
|
||||||
|
Manifest.permission.RECORD_AUDIO -> true
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
Logger.d("All required permissions: ${hasAllRequiredPermissions()}")
|
|
||||||
Logger.d("Camera permissions: ${hasCameraPermissions()}")
|
|
||||||
Logger.d("Critical permissions: ${hasCriticalPermissions()}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Результат запроса разрешений
|
||||||
|
*/
|
||||||
|
sealed class PermissionResult {
|
||||||
|
object AllGranted : PermissionResult()
|
||||||
|
data class SomeGranted(val granted: List<String>, val denied: List<String>) : PermissionResult()
|
||||||
|
data class CriticalDenied(val denied: List<String>) : PermissionResult()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
package com.example.godeye.managers
|
package com.example.godeye.managers
|
||||||
|
|
||||||
import com.example.godeye.models.CameraSession
|
import com.example.godeye.models.*
|
||||||
import com.example.godeye.utils.Logger
|
import com.example.godeye.utils.Logger
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Менеджер для управления активными сессиями с операторами
|
* SessionManager - управление активными сессиями с операторами
|
||||||
|
* Отслеживает состояние WebRTC соединений и сессий камеры
|
||||||
*/
|
*/
|
||||||
class SessionManager {
|
class SessionManager {
|
||||||
|
|
||||||
private val _activeSessions = MutableStateFlow<List<CameraSession>>(emptyList())
|
private val activeSessions = ConcurrentHashMap<String, CameraSession>()
|
||||||
val activeSessions: StateFlow<List<CameraSession>> = _activeSessions.asStateFlow()
|
|
||||||
|
private val _sessions = MutableStateFlow<Map<String, CameraSession>>(emptyMap())
|
||||||
|
val sessions: StateFlow<Map<String, CameraSession>> = _sessions.asStateFlow()
|
||||||
|
|
||||||
|
private val _activeSessionCount = MutableStateFlow(0)
|
||||||
|
val activeSessionCount: StateFlow<Int> = _activeSessionCount.asStateFlow()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Добавить новую сессию
|
* Создание новой сессии при принятии запроса оператора
|
||||||
*/
|
*/
|
||||||
fun addSession(sessionId: String, operatorId: String, cameraType: String) {
|
fun createSession(sessionId: String, operatorId: String, cameraType: String): CameraSession {
|
||||||
val newSession = CameraSession(
|
Logger.step("SESSION_CREATE", "Creating session: $sessionId for operator $operatorId")
|
||||||
|
|
||||||
|
val session = CameraSession(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
operatorId = operatorId,
|
operatorId = operatorId,
|
||||||
cameraType = cameraType,
|
cameraType = cameraType,
|
||||||
@@ -27,126 +36,121 @@ class SessionManager {
|
|||||||
webRTCConnected = false
|
webRTCConnected = false
|
||||||
)
|
)
|
||||||
|
|
||||||
val currentSessions = _activeSessions.value.toMutableList()
|
activeSessions[sessionId] = session
|
||||||
// Удаляем существующую сессию с тем же ID, если есть
|
updateSessionsFlow()
|
||||||
currentSessions.removeAll { it.sessionId == sessionId }
|
|
||||||
currentSessions.add(newSession)
|
|
||||||
_activeSessions.value = currentSessions
|
|
||||||
|
|
||||||
Logger.d("Session added: $sessionId, operator: $operatorId, camera: $cameraType")
|
Logger.step("SESSION_CREATED", "Session created: $sessionId")
|
||||||
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить статус WebRTC соединения для сессии
|
* Обновление статуса WebRTC соединения для сессии
|
||||||
*/
|
*/
|
||||||
fun updateWebRTCStatus(sessionId: String, connected: Boolean) {
|
fun updateWebRTCConnection(sessionId: String, connected: Boolean) {
|
||||||
val currentSessions = _activeSessions.value.toMutableList()
|
activeSessions[sessionId]?.let { session ->
|
||||||
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
|
session.webRTCConnected = connected
|
||||||
|
activeSessions[sessionId] = session
|
||||||
|
updateSessionsFlow()
|
||||||
|
|
||||||
if (sessionIndex != -1) {
|
Logger.step("SESSION_WEBRTC_UPDATED",
|
||||||
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
|
"Session $sessionId WebRTC status updated: $connected")
|
||||||
webRTCConnected = connected
|
|
||||||
)
|
|
||||||
_activeSessions.value = currentSessions
|
|
||||||
Logger.d("WebRTC status updated for session $sessionId: $connected")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Переключить камеру для сессии
|
* Завершение сессии
|
||||||
*/
|
*/
|
||||||
fun switchCameraForSession(sessionId: String, newCameraType: String) {
|
fun endSession(sessionId: String, reason: String = "User ended") {
|
||||||
val currentSessions = _activeSessions.value.toMutableList()
|
activeSessions[sessionId]?.let { session ->
|
||||||
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
|
session.isActive = false
|
||||||
|
activeSessions.remove(sessionId)
|
||||||
|
updateSessionsFlow()
|
||||||
|
|
||||||
if (sessionIndex != -1) {
|
Logger.step("SESSION_ENDED", "Session ended: $sessionId, reason: $reason")
|
||||||
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
|
|
||||||
cameraType = newCameraType
|
|
||||||
)
|
|
||||||
_activeSessions.value = currentSessions
|
|
||||||
Logger.d("Camera switched for session $sessionId to $newCameraType")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Завершить сессию
|
* Получение активной сессии по ID
|
||||||
*/
|
|
||||||
fun endSession(sessionId: String) {
|
|
||||||
val currentSessions = _activeSessions.value.toMutableList()
|
|
||||||
val removed = currentSessions.removeAll { it.sessionId == sessionId }
|
|
||||||
|
|
||||||
if (removed) {
|
|
||||||
_activeSessions.value = currentSessions
|
|
||||||
Logger.d("Session ended: $sessionId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить сессию по ID
|
|
||||||
*/
|
*/
|
||||||
fun getSession(sessionId: String): CameraSession? {
|
fun getSession(sessionId: String): CameraSession? {
|
||||||
return _activeSessions.value.find { it.sessionId == sessionId }
|
return activeSessions[sessionId]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить, есть ли активные сессии
|
* Получение всех активных сессий
|
||||||
|
*/
|
||||||
|
fun getAllActiveSessions(): List<CameraSession> {
|
||||||
|
return activeSessions.values.filter { it.isActive }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка, есть ли активные сессии
|
||||||
*/
|
*/
|
||||||
fun hasActiveSessions(): Boolean {
|
fun hasActiveSessions(): Boolean {
|
||||||
return _activeSessions.value.isNotEmpty()
|
return activeSessions.values.any { it.isActive }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить количество активных сессий
|
* Завершение всех активных сессий
|
||||||
*/
|
*/
|
||||||
fun getActiveSessionCount(): Int {
|
fun endAllSessions(reason: String = "Service stopped") {
|
||||||
return _activeSessions.value.size
|
Logger.step("SESSION_END_ALL", "Ending all active sessions: $reason")
|
||||||
|
|
||||||
|
activeSessions.values.forEach { session ->
|
||||||
|
if (session.isActive) {
|
||||||
|
session.isActive = false
|
||||||
|
Logger.step("SESSION_ENDED", "Session ended: ${session.sessionId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSessions.clear()
|
||||||
|
updateSessionsFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Завершить все сессии
|
* Переключение камеры для сессии
|
||||||
*/
|
*/
|
||||||
fun endAllSessions() {
|
fun switchCamera(sessionId: String, newCameraType: String) {
|
||||||
val sessionIds = _activeSessions.value.map { it.sessionId }
|
activeSessions[sessionId]?.let { session ->
|
||||||
_activeSessions.value = emptyList()
|
session.cameraType = newCameraType
|
||||||
Logger.d("All sessions ended: ${sessionIds.joinToString(", ")}")
|
activeSessions[sessionId] = session
|
||||||
|
updateSessionsFlow()
|
||||||
|
|
||||||
|
Logger.step("SESSION_CAMERA_SWITCHED",
|
||||||
|
"Session $sessionId camera switched to: $newCameraType")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить текущий тип камеры для активной сессии
|
* Получение статистики сессий
|
||||||
*/
|
|
||||||
fun getCurrentCameraType(): String? {
|
|
||||||
return _activeSessions.value.firstOrNull()?.cameraType
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить, подключен ли WebRTC для сессии
|
|
||||||
*/
|
|
||||||
fun isWebRTCConnected(sessionId: String): Boolean {
|
|
||||||
return getSession(sessionId)?.webRTCConnected ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить статистику сессий
|
|
||||||
*/
|
*/
|
||||||
fun getSessionStats(): SessionStats {
|
fun getSessionStats(): SessionStats {
|
||||||
val sessions = _activeSessions.value
|
val active = activeSessions.values.filter { it.isActive }
|
||||||
|
val withWebRTC = active.filter { it.webRTCConnected }
|
||||||
|
|
||||||
return SessionStats(
|
return SessionStats(
|
||||||
totalSessions = sessions.size,
|
totalActive = active.size,
|
||||||
connectedSessions = sessions.count { it.webRTCConnected },
|
webRTCConnected = withWebRTC.size,
|
||||||
activeSessions = sessions.count { it.isActive },
|
operators = active.map { it.operatorId }.distinct().size,
|
||||||
oldestSessionTime = sessions.minOfOrNull { it.startTime },
|
averageDuration = if (active.isNotEmpty()) {
|
||||||
newestSessionTime = sessions.maxOfOrNull { it.startTime }
|
active.map { System.currentTimeMillis() - it.startTime }.average().toLong()
|
||||||
|
} else 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSessionsFlow() {
|
||||||
|
_sessions.value = activeSessions.toMap()
|
||||||
|
_activeSessionCount.value = activeSessions.values.count { it.isActive }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Статистика сессий
|
* Статистика сессий
|
||||||
*/
|
*/
|
||||||
data class SessionStats(
|
data class SessionStats(
|
||||||
val totalSessions: Int,
|
val totalActive: Int,
|
||||||
val connectedSessions: Int,
|
val webRTCConnected: Int,
|
||||||
val activeSessions: Int,
|
val operators: Int,
|
||||||
val oldestSessionTime: Long?,
|
val averageDuration: Long
|
||||||
val newestSessionTime: Long?
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
package com.example.godeye.managers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.example.godeye.models.AppError
|
|
||||||
import com.example.godeye.models.WebRTCConnectionState
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import com.example.godeye.utils.Logger
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Менеджер для управления WebRTC соединениями (заглушка)
|
|
||||||
* В реальном проекте здесь будет полная реализация с WebRTC библиотекой
|
|
||||||
*/
|
|
||||||
class WebRTCManager(private val context: Context) {
|
|
||||||
|
|
||||||
private val _connectionState = MutableStateFlow(WebRTCConnectionState.NEW)
|
|
||||||
val connectionState: StateFlow<WebRTCConnectionState> = _connectionState.asStateFlow()
|
|
||||||
|
|
||||||
private val _error = MutableStateFlow<AppError?>(null)
|
|
||||||
val error: StateFlow<AppError?> = _error.asStateFlow()
|
|
||||||
|
|
||||||
// Callback для передачи событий WebRTC
|
|
||||||
private var onOfferCreated: ((String) -> Unit)? = null
|
|
||||||
private var onAnswerCreated: ((String) -> Unit)? = null
|
|
||||||
private var onIceCandidateCreated: ((String, String, Int) -> Unit)? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Инициализация WebRTC (заглушка)
|
|
||||||
*/
|
|
||||||
fun initialize() {
|
|
||||||
try {
|
|
||||||
Logger.d("WebRTC initialized (stub implementation)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error initializing WebRTC", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать PeerConnection (заглушка)
|
|
||||||
*/
|
|
||||||
fun createPeerConnection(
|
|
||||||
onOfferCreated: (String) -> Unit,
|
|
||||||
onAnswerCreated: (String) -> Unit,
|
|
||||||
onIceCandidateCreated: (String, String, Int) -> Unit
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
this.onOfferCreated = onOfferCreated
|
|
||||||
this.onAnswerCreated = onAnswerCreated
|
|
||||||
this.onIceCandidateCreated = onIceCandidateCreated
|
|
||||||
|
|
||||||
Logger.d("PeerConnection created (stub implementation)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error creating PeerConnection", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать локальные медиа треки (заглушка)
|
|
||||||
*/
|
|
||||||
fun createLocalMediaTracks(cameraType: String) {
|
|
||||||
try {
|
|
||||||
Logger.d("Local media tracks created for $cameraType (stub implementation)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error creating local media tracks", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать Offer (заглушка)
|
|
||||||
*/
|
|
||||||
fun createOffer() {
|
|
||||||
try {
|
|
||||||
// Симулируем создание offer
|
|
||||||
val mockOffer = "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 127.0.0.1\r\na=rtcp:9 IN IP4 127.0.0.1"
|
|
||||||
onOfferCreated?.invoke(mockOffer)
|
|
||||||
Logger.d("Offer created (stub implementation)")
|
|
||||||
|
|
||||||
// Симулируем успешное соединение через некоторое время
|
|
||||||
_connectionState.value = WebRTCConnectionState.CONNECTED
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error creating offer", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать Answer (заглушка)
|
|
||||||
*/
|
|
||||||
fun handleAnswer(answerSdp: String) {
|
|
||||||
try {
|
|
||||||
Logger.d("Answer processed (stub implementation): ${answerSdp.take(50)}...")
|
|
||||||
_connectionState.value = WebRTCConnectionState.CONNECTED
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error handling answer", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавить ICE candidate (заглушка)
|
|
||||||
*/
|
|
||||||
fun addIceCandidate(candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) {
|
|
||||||
try {
|
|
||||||
Logger.d("ICE candidate added (stub implementation): $candidateSdp")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error adding ICE candidate", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Переключить камеру (заглушка)
|
|
||||||
*/
|
|
||||||
fun switchCamera(newCameraType: String) {
|
|
||||||
try {
|
|
||||||
Logger.d("Camera switched to: $newCameraType (stub implementation)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error switching camera", e)
|
|
||||||
_error.value = AppError.CameraError("Failed to switch camera: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Закрыть WebRTC соединение (заглушка)
|
|
||||||
*/
|
|
||||||
fun close() {
|
|
||||||
try {
|
|
||||||
_connectionState.value = WebRTCConnectionState.CLOSED
|
|
||||||
Logger.d("WebRTC connection closed (stub implementation)")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error closing WebRTC connection", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить ошибку
|
|
||||||
*/
|
|
||||||
fun clearError() {
|
|
||||||
_error.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
app/src/main/java/com/example/godeye/models/CameraModels.kt
Normal file
26
app/src/main/java/com/example/godeye/models/CameraModels.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.example.godeye.models
|
||||||
|
|
||||||
|
import android.util.Size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Состояния камеры согласно ТЗ
|
||||||
|
*/
|
||||||
|
enum class CameraState {
|
||||||
|
CLOSED, // Камера закрыта
|
||||||
|
OPENING, // Камера открывается
|
||||||
|
OPENED, // Камера открыта
|
||||||
|
CONFIGURING, // Настройка сессии захвата
|
||||||
|
ACTIVE, // Камера активна и передает видео
|
||||||
|
ERROR // Ошибка камеры
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Информация о камере устройства согласно ТЗ
|
||||||
|
*/
|
||||||
|
data class CameraInfo(
|
||||||
|
val id: String, // ID камеры в системе
|
||||||
|
val type: String, // Тип камеры: back, front, ultra_wide, telephoto
|
||||||
|
val facing: Int, // Направление камеры (LENS_FACING_*)
|
||||||
|
val supportedSizes: List<Size>, // Поддерживаемые разрешения
|
||||||
|
val focalLengths: List<Float> // Фокусные расстояния для определения типа
|
||||||
|
)
|
||||||
@@ -1,154 +1,55 @@
|
|||||||
package com.example.godeye.models
|
package com.example.godeye.models
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Информация об устройстве для регистрации на сервере
|
|
||||||
*/
|
|
||||||
data class DeviceInfo(
|
|
||||||
val model: String = Build.MODEL,
|
|
||||||
val androidVersion: String = Build.VERSION.RELEASE,
|
|
||||||
val appVersion: String = "1.0.0", // Заменяем BuildConfig на хардкод для упрощения
|
|
||||||
val availableCameras: List<String>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Активная сессия с оператором
|
|
||||||
*/
|
|
||||||
data class CameraSession(
|
|
||||||
val sessionId: String,
|
|
||||||
val operatorId: String,
|
|
||||||
val cameraType: String,
|
|
||||||
val startTime: Long,
|
|
||||||
var isActive: Boolean = true,
|
|
||||||
var webRTCConnected: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запрос доступа к камере от оператора
|
|
||||||
*/
|
|
||||||
data class CameraRequest(
|
|
||||||
val sessionId: String,
|
|
||||||
val operatorId: String,
|
|
||||||
val cameraType: String,
|
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответ на запрос доступа к камере
|
|
||||||
*/
|
|
||||||
data class CameraResponse(
|
|
||||||
val sessionId: String,
|
|
||||||
val accepted: Boolean,
|
|
||||||
val reason: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebRTC Offer/Answer данные
|
|
||||||
*/
|
|
||||||
data class WebRTCMessage(
|
|
||||||
val sessionId: String,
|
|
||||||
val type: String, // "offer", "answer", "ice-candidate"
|
|
||||||
val sdp: String? = null,
|
|
||||||
val candidate: String? = null,
|
|
||||||
val sdpMid: String? = null,
|
|
||||||
val sdpMLineIndex: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* События Socket.IO
|
|
||||||
*/
|
|
||||||
sealed class SocketEvent {
|
|
||||||
data class RegisterAndroid(
|
|
||||||
val deviceId: String,
|
|
||||||
val deviceInfo: DeviceInfo
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class CameraRequest(
|
|
||||||
val sessionId: String,
|
|
||||||
val operatorId: String,
|
|
||||||
val cameraType: String
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class CameraResponse(
|
|
||||||
val sessionId: String,
|
|
||||||
val accepted: Boolean,
|
|
||||||
val reason: String? = null
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class CameraDisconnect(
|
|
||||||
val sessionId: String
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class CameraSwitch(
|
|
||||||
val sessionId: String,
|
|
||||||
val newCameraType: String
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class WebRTCOffer(
|
|
||||||
val sessionId: String,
|
|
||||||
val offer: String
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class WebRTCAnswer(
|
|
||||||
val sessionId: String,
|
|
||||||
val answer: String
|
|
||||||
) : SocketEvent()
|
|
||||||
|
|
||||||
data class WebRTCIceCandidate(
|
|
||||||
val sessionId: String,
|
|
||||||
val candidate: String,
|
|
||||||
val sdpMid: String,
|
|
||||||
val sdpMLineIndex: Int
|
|
||||||
) : SocketEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Состояния подключения
|
|
||||||
*/
|
|
||||||
enum class ConnectionState {
|
enum class ConnectionState {
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
CONNECTING,
|
CONNECTING,
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
ERROR,
|
RECONNECTING,
|
||||||
RECONNECTING
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
data class CameraResponse(
|
||||||
* Состояния WebRTC соединения
|
val sessionId: String,
|
||||||
*/
|
val accepted: Boolean,
|
||||||
enum class WebRTCConnectionState {
|
val reason: String? = null,
|
||||||
NEW,
|
val streamUrl: String? = null
|
||||||
CONNECTING,
|
|
||||||
CONNECTED,
|
|
||||||
DISCONNECTED,
|
|
||||||
FAILED,
|
|
||||||
CLOSED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Типы ошибок приложения
|
|
||||||
*/
|
|
||||||
sealed class AppError {
|
|
||||||
object NetworkError : AppError()
|
|
||||||
object CameraPermissionDenied : AppError()
|
|
||||||
object AudioPermissionDenied : AppError()
|
|
||||||
object CameraNotAvailable : AppError()
|
|
||||||
object WebRTCConnectionFailed : AppError()
|
|
||||||
data class SocketError(val message: String) : AppError()
|
|
||||||
data class CameraError(val message: String) : AppError()
|
|
||||||
data class UnknownError(val throwable: Throwable) : AppError()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI состояние главного экрана
|
|
||||||
*/
|
|
||||||
data class MainScreenState(
|
|
||||||
val deviceId: String = "",
|
|
||||||
val serverUrl: String = "",
|
|
||||||
val connectionState: ConnectionState = ConnectionState.DISCONNECTED,
|
|
||||||
val activeSessions: List<CameraSession> = emptyList(),
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val error: AppError? = null,
|
|
||||||
val showCameraRequest: CameraRequest? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SessionInfo(
|
||||||
|
val sessionId: String,
|
||||||
|
val deviceId: String,
|
||||||
|
val operatorId: String,
|
||||||
|
val cameraType: String,
|
||||||
|
val status: String,
|
||||||
|
val createdAt: String,
|
||||||
|
val acceptedAt: String? = null,
|
||||||
|
val endedAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CameraSwitchRequest(
|
||||||
|
val sessionId: String,
|
||||||
|
val cameraType: String
|
||||||
|
)
|
||||||
|
|
||||||
|
object SocketEvents {
|
||||||
|
const val REGISTER_ANDROID = "register:android"
|
||||||
|
const val REGISTER_SUCCESS = "register:success"
|
||||||
|
const val REGISTER_ERROR = "register:error"
|
||||||
|
const val CAMERA_REQUEST = "camera:request"
|
||||||
|
const val CAMERA_RESPONSE = "camera:response"
|
||||||
|
const val CAMERA_SWITCH = "camera:switch"
|
||||||
|
const val CAMERA_DISCONNECT = "camera:disconnect"
|
||||||
|
const val SESSION_CREATED = "session:created"
|
||||||
|
const val SESSION_ACCEPTED = "session:accepted"
|
||||||
|
const val SESSION_REJECTED = "session:rejected"
|
||||||
|
const val SESSION_ENDED = "session:ended"
|
||||||
|
const val SERVER_HELLO = "server:hello"
|
||||||
|
const val WEBRTC_OFFER = "webrtc:offer"
|
||||||
|
const val WEBRTC_ANSWER = "webrtc:answer"
|
||||||
|
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
|
||||||
|
const val DEVICE_CONNECTED = "device:connected"
|
||||||
|
const val DEVICE_DISCONNECTED = "device:disconnected"
|
||||||
|
const val HEARTBEAT = "heartbeat"
|
||||||
|
const val HEARTBEAT_ACK = "heartbeat:ack"
|
||||||
|
const val ERROR = "error"
|
||||||
|
}
|
||||||
|
|||||||
62
app/src/main/java/com/example/godeye/models/SocketEvents.kt
Normal file
62
app/src/main/java/com/example/godeye/models/SocketEvents.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package com.example.godeye.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Информация об Android устройстве для регистрации на сервере
|
||||||
|
* Соответствует требованиям ТЗ для Socket.IO регистрации
|
||||||
|
*/
|
||||||
|
data class DeviceInfo(
|
||||||
|
val model: String,
|
||||||
|
val androidVersion: String,
|
||||||
|
val appVersion: String,
|
||||||
|
val availableCameras: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Активная сессия камеры с оператором
|
||||||
|
* Соответствует требованиям ТЗ для управления WebRTC сессиями
|
||||||
|
*/
|
||||||
|
data class CameraSession(
|
||||||
|
val sessionId: String,
|
||||||
|
val operatorId: String,
|
||||||
|
var cameraType: String,
|
||||||
|
val startTime: Long,
|
||||||
|
var isActive: Boolean = true,
|
||||||
|
var webRTCConnected: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запрос доступа к камере от оператора
|
||||||
|
* Получается через Socket.IO событие "camera:request"
|
||||||
|
*/
|
||||||
|
data class CameraRequest(
|
||||||
|
val sessionId: String,
|
||||||
|
val operatorId: String,
|
||||||
|
val cameraType: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* События Socket.IO для типизированной обработки
|
||||||
|
* Соответствует архитектуре ТЗ с WebSocket сигнализацией
|
||||||
|
*/
|
||||||
|
sealed class SocketEvent {
|
||||||
|
data class RegisterAndroid(val deviceId: String, val deviceInfo: DeviceInfo) : SocketEvent()
|
||||||
|
data class CameraRequestEvent(val sessionId: String, val operatorId: String, val cameraType: String) : SocketEvent()
|
||||||
|
data class CameraResponse(val sessionId: String, val accepted: Boolean, val reason: String = "") : SocketEvent()
|
||||||
|
data class WebRTCOffer(val sessionId: String, val offer: String) : SocketEvent()
|
||||||
|
data class WebRTCAnswer(val sessionId: String, val answer: String) : SocketEvent()
|
||||||
|
data class IceCandidate(val sessionId: String, val candidate: String, val sdpMid: String, val sdpMLineIndex: Int) : SocketEvent()
|
||||||
|
data class CameraSwitch(val sessionId: String, val cameraType: String) : SocketEvent()
|
||||||
|
data class SessionEnd(val sessionId: String, val reason: String) : SocketEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ошибки приложения согласно ТЗ
|
||||||
|
*/
|
||||||
|
sealed class AppError {
|
||||||
|
object NetworkError : AppError()
|
||||||
|
object CameraPermissionDenied : AppError()
|
||||||
|
object CameraNotAvailable : AppError()
|
||||||
|
object WebRTCConnectionFailed : AppError()
|
||||||
|
data class SocketError(val message: String) : AppError()
|
||||||
|
data class UnknownError(val throwable: Throwable) : AppError()
|
||||||
|
}
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
package com.example.godeye.services
|
|
||||||
|
|
||||||
import android.app.*
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.os.Binder
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.view.Surface
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.example.godeye.MainActivity
|
|
||||||
import com.example.godeye.R
|
|
||||||
import com.example.godeye.managers.CameraManager
|
|
||||||
import com.example.godeye.managers.SessionManager
|
|
||||||
import com.example.godeye.managers.WebRTCManager
|
|
||||||
import com.example.godeye.models.AppError
|
|
||||||
import com.example.godeye.models.WebRTCConnectionState
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import com.example.godeye.utils.Logger
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Сервис для управления камерой и WebRTC соединениями
|
|
||||||
*/
|
|
||||||
class CameraService : Service() {
|
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
|
||||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
||||||
|
|
||||||
private lateinit var cameraManager: CameraManager
|
|
||||||
private lateinit var webRTCManager: WebRTCManager
|
|
||||||
private lateinit var sessionManager: SessionManager
|
|
||||||
|
|
||||||
// Surface для WebRTC видео
|
|
||||||
private var webRTCSurface: Surface? = null
|
|
||||||
private var surfaceTexture: SurfaceTexture? = null
|
|
||||||
|
|
||||||
// StateFlows для отслеживания состояния
|
|
||||||
private val _isActive = MutableStateFlow(false)
|
|
||||||
val isActive: StateFlow<Boolean> = _isActive.asStateFlow()
|
|
||||||
|
|
||||||
private val _error = MutableStateFlow<AppError?>(null)
|
|
||||||
val error: StateFlow<AppError?> = _error.asStateFlow()
|
|
||||||
|
|
||||||
// Callbacks для передачи WebRTC событий
|
|
||||||
private var onWebRTCOfferCreated: ((String, String) -> Unit)? = null // sessionId, offer
|
|
||||||
private var onWebRTCIceCandidateCreated: ((String, String, String, Int) -> Unit)? = null // sessionId, candidate, sdpMid, sdpMLineIndex
|
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
|
||||||
fun getService(): CameraService = this@CameraService
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
Logger.d("CameraService created")
|
|
||||||
|
|
||||||
cameraManager = CameraManager(this)
|
|
||||||
webRTCManager = WebRTCManager(this)
|
|
||||||
sessionManager = SessionManager()
|
|
||||||
|
|
||||||
// Инициализация WebRTC
|
|
||||||
webRTCManager.initialize()
|
|
||||||
|
|
||||||
createNotificationChannel()
|
|
||||||
observeManagerStates()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder = binder
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
startForeground(Constants.FOREGROUND_SERVICE_ID + 1, createNotification())
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Установить callbacks для WebRTC событий
|
|
||||||
*/
|
|
||||||
fun setWebRTCCallbacks(
|
|
||||||
onOfferCreated: (String, String) -> Unit,
|
|
||||||
onIceCandidateCreated: (String, String, String, Int) -> Unit
|
|
||||||
) {
|
|
||||||
this.onWebRTCOfferCreated = onOfferCreated
|
|
||||||
this.onWebRTCIceCandidateCreated = onIceCandidateCreated
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Начать камера сессию
|
|
||||||
*/
|
|
||||||
fun startCameraSession(sessionId: String, operatorId: String, cameraType: String) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Starting camera session: $sessionId, camera: $cameraType")
|
|
||||||
|
|
||||||
// Добавляем сессию в менеджер
|
|
||||||
sessionManager.addSession(sessionId, operatorId, cameraType)
|
|
||||||
|
|
||||||
// Создаем Surface для WebRTC
|
|
||||||
setupWebRTCSurface()
|
|
||||||
|
|
||||||
// Создаем WebRTC соединение
|
|
||||||
webRTCManager.createPeerConnection(
|
|
||||||
onOfferCreated = { offer ->
|
|
||||||
onWebRTCOfferCreated?.invoke(sessionId, offer)
|
|
||||||
},
|
|
||||||
onAnswerCreated = { answer ->
|
|
||||||
// Ответ не используется, так как мы создаем offer
|
|
||||||
},
|
|
||||||
onIceCandidateCreated = { candidate, sdpMid, sdpMLineIndex ->
|
|
||||||
onWebRTCIceCandidateCreated?.invoke(sessionId, candidate, sdpMid, sdpMLineIndex)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Создаем локальные медиа треки
|
|
||||||
webRTCManager.createLocalMediaTracks(cameraType)
|
|
||||||
|
|
||||||
// Открываем камеру
|
|
||||||
webRTCSurface?.let { surface ->
|
|
||||||
cameraManager.openCamera(
|
|
||||||
cameraType = cameraType,
|
|
||||||
surface = surface,
|
|
||||||
onSuccess = {
|
|
||||||
Logger.d("Camera opened successfully for session: $sessionId")
|
|
||||||
_isActive.value = true
|
|
||||||
|
|
||||||
// Создаем WebRTC offer
|
|
||||||
webRTCManager.createOffer()
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
Logger.e("Failed to open camera for session: $sessionId")
|
|
||||||
_error.value = error
|
|
||||||
sessionManager.endSession(sessionId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} ?: run {
|
|
||||||
val error = AppError.CameraError("WebRTC surface not available")
|
|
||||||
_error.value = error
|
|
||||||
sessionManager.endSession(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error starting camera session", e)
|
|
||||||
_error.value = AppError.UnknownError(e)
|
|
||||||
sessionManager.endSession(sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать WebRTC answer
|
|
||||||
*/
|
|
||||||
fun handleWebRTCAnswer(sessionId: String, answer: String) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Handling WebRTC answer for session: $sessionId")
|
|
||||||
webRTCManager.handleAnswer(answer)
|
|
||||||
sessionManager.updateWebRTCStatus(sessionId, true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error handling WebRTC answer", e)
|
|
||||||
_error.value = AppError.WebRTCConnectionFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавить ICE candidate
|
|
||||||
*/
|
|
||||||
fun addIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Adding ICE candidate for session: $sessionId")
|
|
||||||
webRTCManager.addIceCandidate(candidate, sdpMid, sdpMLineIndex)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error adding ICE candidate", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Переключить камеру
|
|
||||||
*/
|
|
||||||
fun switchCamera(sessionId: String, newCameraType: String) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Switching camera for session $sessionId to $newCameraType")
|
|
||||||
|
|
||||||
// Обновляем тип камеры в сессии
|
|
||||||
sessionManager.switchCameraForSession(sessionId, newCameraType)
|
|
||||||
|
|
||||||
// Переключаем камеру в WebRTC
|
|
||||||
webRTCManager.switchCamera(newCameraType)
|
|
||||||
|
|
||||||
// Переключаем физическую камеру
|
|
||||||
webRTCSurface?.let { surface ->
|
|
||||||
cameraManager.switchCamera(
|
|
||||||
newCameraType = newCameraType,
|
|
||||||
surface = surface,
|
|
||||||
onSuccess = {
|
|
||||||
Logger.d("Camera switched successfully to: $newCameraType")
|
|
||||||
},
|
|
||||||
onError = { error ->
|
|
||||||
Logger.e("Failed to switch camera to: $newCameraType")
|
|
||||||
_error.value = error
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error switching camera", e)
|
|
||||||
_error.value = AppError.CameraError("Failed to switch camera: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Завершить сессию
|
|
||||||
*/
|
|
||||||
fun endSession(sessionId: String) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Ending session: $sessionId")
|
|
||||||
|
|
||||||
// Закрываем камеру
|
|
||||||
cameraManager.closeCamera()
|
|
||||||
|
|
||||||
// Закрываем WebRTC соединение
|
|
||||||
webRTCManager.close()
|
|
||||||
|
|
||||||
// Удаляем сессию
|
|
||||||
sessionManager.endSession(sessionId)
|
|
||||||
|
|
||||||
// Очищаем Surface
|
|
||||||
cleanupWebRTCSurface()
|
|
||||||
|
|
||||||
_isActive.value = false
|
|
||||||
Logger.d("Session ended successfully: $sessionId")
|
|
||||||
|
|
||||||
// Если нет активных сессий, останавливаем сервис
|
|
||||||
if (!sessionManager.hasActiveSessions()) {
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error ending session", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Завершить все сессии
|
|
||||||
*/
|
|
||||||
fun endAllSessions() {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
Logger.d("Ending all sessions")
|
|
||||||
|
|
||||||
// Закрываем камеру
|
|
||||||
cameraManager.closeCamera()
|
|
||||||
|
|
||||||
// Закрываем WebRTC соединение
|
|
||||||
webRTCManager.close()
|
|
||||||
|
|
||||||
// Удаляем все сессии
|
|
||||||
sessionManager.endAllSessions()
|
|
||||||
|
|
||||||
// Очищаем Surface
|
|
||||||
cleanupWebRTCSurface()
|
|
||||||
|
|
||||||
_isActive.value = false
|
|
||||||
Logger.d("All sessions ended successfully")
|
|
||||||
|
|
||||||
stopSelf()
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error ending all sessions", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Настроить Surface для WebRTC
|
|
||||||
*/
|
|
||||||
private fun setupWebRTCSurface() {
|
|
||||||
try {
|
|
||||||
// Создаем SurfaceTexture для WebRTC
|
|
||||||
surfaceTexture = SurfaceTexture(0).apply {
|
|
||||||
setDefaultBufferSize(1280, 720)
|
|
||||||
}
|
|
||||||
webRTCSurface = Surface(surfaceTexture)
|
|
||||||
Logger.d("WebRTC surface created successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error creating WebRTC surface", e)
|
|
||||||
_error.value = AppError.CameraError("Failed to create WebRTC surface: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить WebRTC Surface
|
|
||||||
*/
|
|
||||||
private fun cleanupWebRTCSurface() {
|
|
||||||
try {
|
|
||||||
webRTCSurface?.release()
|
|
||||||
webRTCSurface = null
|
|
||||||
|
|
||||||
surfaceTexture?.release()
|
|
||||||
surfaceTexture = null
|
|
||||||
|
|
||||||
Logger.d("WebRTC surface cleaned up")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error cleaning up WebRTC surface", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Наблюдать за состояниями менеджеров
|
|
||||||
*/
|
|
||||||
private fun observeManagerStates() {
|
|
||||||
serviceScope.launch {
|
|
||||||
// Наблюдаем за ошибками камеры
|
|
||||||
cameraManager.error.collect { error ->
|
|
||||||
error?.let {
|
|
||||||
_error.value = it
|
|
||||||
Logger.e("Camera manager error: $it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceScope.launch {
|
|
||||||
// Наблюдаем за ошибками WebRTC
|
|
||||||
webRTCManager.error.collect { error ->
|
|
||||||
error?.let {
|
|
||||||
_error.value = it
|
|
||||||
Logger.e("WebRTC manager error: $it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceScope.launch {
|
|
||||||
// Наблюдаем за состоянием WebRTC соединения
|
|
||||||
webRTCManager.connectionState.collect { state ->
|
|
||||||
Logger.d("WebRTC connection state: $state")
|
|
||||||
when (state) {
|
|
||||||
WebRTCConnectionState.CONNECTED -> {
|
|
||||||
// Обновляем статус всех активных сессий
|
|
||||||
sessionManager.activeSessions.value.forEach { session ->
|
|
||||||
sessionManager.updateWebRTCStatus(session.sessionId, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WebRTCConnectionState.FAILED,
|
|
||||||
WebRTCConnectionState.DISCONNECTED -> {
|
|
||||||
// Обновляем статус всех активных сессий
|
|
||||||
sessionManager.activeSessions.value.forEach { session ->
|
|
||||||
sessionManager.updateWebRTCStatus(session.sessionId, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> { /* Игнорируем другие состояния */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить менеджер сессий
|
|
||||||
*/
|
|
||||||
fun getSessionManager(): SessionManager = sessionManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать канал уведомлений
|
|
||||||
*/
|
|
||||||
private fun createNotificationChannel() {
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
"${Constants.NOTIFICATION_CHANNEL_ID}_camera",
|
|
||||||
"GodEye Camera Service",
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
description = "Уведомления о работе камеры GodEye"
|
|
||||||
setShowBadge(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать уведомление для foreground service
|
|
||||||
*/
|
|
||||||
private fun createNotification(): Notification {
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
val activeSessionsCount = sessionManager.getActiveSessionCount()
|
|
||||||
val statusText = if (activeSessionsCount > 0) {
|
|
||||||
"Активных сессий: $activeSessionsCount"
|
|
||||||
} else {
|
|
||||||
"Камера готова к работе"
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotificationCompat.Builder(this, "${Constants.NOTIFICATION_CHANNEL_ID}_camera")
|
|
||||||
.setContentTitle("GodEye Camera")
|
|
||||||
.setContentText(statusText)
|
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setOngoing(true)
|
|
||||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить ошибку
|
|
||||||
*/
|
|
||||||
fun clearError() {
|
|
||||||
_error.value = null
|
|
||||||
cameraManager.clearError()
|
|
||||||
webRTCManager.clearError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
serviceScope.launch {
|
|
||||||
endAllSessions()
|
|
||||||
}
|
|
||||||
Logger.d("CameraService destroyed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
package com.example.godeye.services
|
package com.example.godeye.services
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Notification
|
||||||
import android.content.Context
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.example.godeye.MainActivity
|
|
||||||
import com.example.godeye.R
|
import com.example.godeye.R
|
||||||
import com.example.godeye.managers.PermissionManager
|
|
||||||
import com.example.godeye.models.*
|
import com.example.godeye.models.*
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import com.example.godeye.utils.Logger
|
import com.example.godeye.utils.Logger
|
||||||
import com.example.godeye.utils.generateDeviceId
|
|
||||||
import com.example.godeye.utils.getAvailableCameraTypes
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import io.socket.client.IO
|
import io.socket.client.IO
|
||||||
@@ -25,11 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис для управления WebSocket соединением с backend сервером
|
* SocketService - основной сервис для WebSocket соединения с backend сервером
|
||||||
|
* Работает в фоне и обеспечивает постоянное соединение с сервером
|
||||||
*/
|
*/
|
||||||
class SocketService : Service() {
|
class SocketService : Service() {
|
||||||
|
|
||||||
@@ -38,393 +38,359 @@ class SocketService : Service() {
|
|||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
private lateinit var permissionManager: PermissionManager
|
// Состояния сервиса
|
||||||
|
|
||||||
// StateFlows для отслеживания состояния
|
|
||||||
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
private val _deviceId = MutableStateFlow("")
|
private val _deviceId = MutableStateFlow("")
|
||||||
val deviceId: StateFlow<String> = _deviceId.asStateFlow()
|
val deviceId: StateFlow<String> = _deviceId.asStateFlow()
|
||||||
|
|
||||||
private val _error = MutableStateFlow<AppError?>(null)
|
// События для передачи в UI
|
||||||
val error: StateFlow<AppError?> = _error.asStateFlow()
|
private val _cameraRequests = MutableStateFlow<CameraRequest?>(null)
|
||||||
|
val cameraRequests: StateFlow<CameraRequest?> = _cameraRequests.asStateFlow()
|
||||||
|
|
||||||
// События для UI
|
private val _webRTCEvents = MutableStateFlow<WebRTCEvent?>(null)
|
||||||
private val _cameraRequest = MutableStateFlow<CameraRequest?>(null)
|
val webRTCEvents: StateFlow<WebRTCEvent?> = _webRTCEvents.asStateFlow()
|
||||||
val cameraRequest: StateFlow<CameraRequest?> = _cameraRequest.asStateFlow()
|
|
||||||
|
|
||||||
private val _webrtcOffer = MutableStateFlow<WebRTCMessage?>(null)
|
|
||||||
val webrtcOffer: StateFlow<WebRTCMessage?> = _webrtcOffer.asStateFlow()
|
|
||||||
|
|
||||||
private val _webrtcAnswer = MutableStateFlow<WebRTCMessage?>(null)
|
|
||||||
val webrtcAnswer: StateFlow<WebRTCMessage?> = _webrtcAnswer.asStateFlow()
|
|
||||||
|
|
||||||
private val _webrtcIceCandidate = MutableStateFlow<WebRTCMessage?>(null)
|
|
||||||
val webrtcIceCandidate: StateFlow<WebRTCMessage?> = _webrtcIceCandidate.asStateFlow()
|
|
||||||
|
|
||||||
private val _cameraSwitchRequest = MutableStateFlow<Pair<String, String>?>(null) // sessionId, newCameraType
|
|
||||||
val cameraSwitchRequest: StateFlow<Pair<String, String>?> = _cameraSwitchRequest.asStateFlow()
|
|
||||||
|
|
||||||
private val _sessionDisconnect = MutableStateFlow<String?>(null) // sessionId
|
|
||||||
val sessionDisconnect: StateFlow<String?> = _sessionDisconnect.asStateFlow()
|
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
fun getService(): SocketService = this@SocketService
|
fun getService(): SocketService = this@SocketService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder = binder
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Logger.d("SocketService created")
|
Logger.step("SOCKET_SERVICE_CREATE", "SocketService created")
|
||||||
permissionManager = PermissionManager(this)
|
|
||||||
_deviceId.value = generateDeviceId()
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
startForeground(NOTIFICATION_ID, createNotification())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder = binder
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
startForeground(Constants.FOREGROUND_SERVICE_ID, createNotification())
|
Logger.step("SOCKET_SERVICE_START", "SocketService started")
|
||||||
return START_STICKY
|
return START_STICKY // Перезапускать сервис при убийстве системой
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Подключиться к серверу
|
* Подключение к backend серверу по Socket.IO
|
||||||
*/
|
*/
|
||||||
fun connect(serverUrl: String) {
|
fun connect(serverUrl: String, deviceId: String) {
|
||||||
|
Logger.step("SOCKET_CONNECT", "Connecting to server: $serverUrl")
|
||||||
|
_deviceId.value = deviceId
|
||||||
|
_connectionState.value = ConnectionState.CONNECTING
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
_connectionState.value = ConnectionState.CONNECTING
|
|
||||||
Logger.d("Connecting to server: $serverUrl")
|
|
||||||
|
|
||||||
// Дополнительная проверка URL
|
|
||||||
if (serverUrl.isBlank()) {
|
|
||||||
Logger.e("Server URL is empty")
|
|
||||||
_connectionState.value = ConnectionState.ERROR
|
|
||||||
_error.value = AppError.NetworkError
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d("Creating URI from: $serverUrl")
|
|
||||||
val uri = URI.create(serverUrl)
|
val uri = URI.create(serverUrl)
|
||||||
Logger.d("URI created successfully: $uri")
|
|
||||||
|
|
||||||
Logger.d("Creating Socket.IO client")
|
|
||||||
val options = IO.Options().apply {
|
val options = IO.Options().apply {
|
||||||
timeout = 10000 // Увеличиваем таймаут до 10 секунд
|
timeout = 10000
|
||||||
reconnection = true
|
reconnection = true
|
||||||
reconnectionDelay = 2000 // Увеличиваем задержку между попытками
|
reconnectionAttempts = 5
|
||||||
reconnectionAttempts = 3 // Уменьшаем количество попыток
|
reconnectionDelay = 1000
|
||||||
forceNew = true // Принудительно создаваем новое соединение
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = IO.socket(uri, options).apply {
|
socket = IO.socket(uri, options)
|
||||||
Logger.d("Socket.IO client created, setting up listeners")
|
setupEventListeners()
|
||||||
setupEventListeners()
|
socket?.connect()
|
||||||
Logger.d("Listeners set up, initiating connection")
|
|
||||||
connect()
|
|
||||||
Logger.d("Connection initiated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем таймаут для проверки подключения
|
|
||||||
launch {
|
|
||||||
kotlinx.coroutines.delay(15000) // Ждем 15 секунд
|
|
||||||
if (_connectionState.value == ConnectionState.CONNECTING) {
|
|
||||||
Logger.w("Connection timeout after 15 seconds")
|
|
||||||
_connectionState.value = ConnectionState.ERROR
|
|
||||||
_error.value = AppError.SocketError("Connection timeout - server may be unreachable")
|
|
||||||
socket?.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e("Error connecting to server: ${e.message}", e)
|
Logger.error("SOCKET_CONNECT_ERROR", "Failed to connect to server", e)
|
||||||
_connectionState.value = ConnectionState.ERROR
|
_connectionState.value = ConnectionState.ERROR
|
||||||
_error.value = AppError.SocketError("Connection failed: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Отключиться от сервера
|
* Настройка обработчиков событий Socket.IO
|
||||||
*/
|
|
||||||
fun disconnect() {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
socket?.disconnect()
|
|
||||||
socket?.close()
|
|
||||||
socket = null
|
|
||||||
_connectionState.value = ConnectionState.DISCONNECTED
|
|
||||||
Logger.d("Disconnected from server")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error disconnecting from server", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Настроить обработчики событий Socket.IO
|
|
||||||
*/
|
*/
|
||||||
private fun setupEventListeners() {
|
private fun setupEventListeners() {
|
||||||
socket?.apply {
|
socket?.apply {
|
||||||
Logger.d("Setting up Socket.IO event listeners")
|
// Событие подключения
|
||||||
|
|
||||||
on(Socket.EVENT_CONNECT) {
|
on(Socket.EVENT_CONNECT) {
|
||||||
Logger.d("✅ Socket connected successfully")
|
Logger.step("SOCKET_CONNECTED", "Connected to server")
|
||||||
_connectionState.value = ConnectionState.CONNECTED
|
_connectionState.value = ConnectionState.CONNECTED
|
||||||
registerDevice()
|
registerDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Socket.EVENT_DISCONNECT) { args ->
|
// Событие отключения
|
||||||
val reason = args.firstOrNull()?.toString() ?: "unknown"
|
on(Socket.EVENT_DISCONNECT) {
|
||||||
Logger.d("❌ Socket disconnected: $reason")
|
Logger.step("SOCKET_DISCONNECTED", "Disconnected from server")
|
||||||
_connectionState.value = ConnectionState.DISCONNECTED
|
_connectionState.value = ConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Socket.EVENT_CONNECT_ERROR) { args ->
|
// Событие подключения (исправлено)
|
||||||
val error = args.firstOrNull()?.toString() ?: "Unknown connection error"
|
on(Socket.EVENT_CONNECT) {
|
||||||
Logger.e("🔥 Socket connection error: $error")
|
Logger.step("SOCKET_RECONNECTED", "Reconnected to server")
|
||||||
_connectionState.value = ConnectionState.ERROR
|
_connectionState.value = ConnectionState.CONNECTED
|
||||||
_error.value = AppError.SocketError(error)
|
registerDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Constants.SocketEvents.REGISTER_SUCCESS) { args ->
|
// Успешная регистрация устройства
|
||||||
Logger.d("Device registered successfully")
|
on("register:success") { args ->
|
||||||
val data = args.firstOrNull()?.toString()
|
Logger.step("REGISTER_SUCCESS", "Device registered successfully")
|
||||||
Logger.d("Registration response: $data")
|
val response = args[0] as JSONObject
|
||||||
|
Logger.d("Registration response: $response")
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Constants.SocketEvents.REGISTER_ERROR) { args ->
|
// Запрос доступа к камере от оператора
|
||||||
val error = args.firstOrNull()?.toString() ?: "Registration failed"
|
on("camera:request") { args ->
|
||||||
Logger.e("Device registration error: $error")
|
|
||||||
_error.value = AppError.SocketError(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
on(Constants.SocketEvents.CAMERA_REQUEST) { args ->
|
|
||||||
try {
|
try {
|
||||||
val data = JSONObject(args[0].toString())
|
val requestData = args[0] as JSONObject
|
||||||
val request = CameraRequest(
|
val cameraRequest = CameraRequest(
|
||||||
sessionId = data.getString("sessionId"),
|
sessionId = requestData.getString("sessionId"),
|
||||||
operatorId = data.getString("operatorId"),
|
operatorId = requestData.getString("operatorId"),
|
||||||
cameraType = data.getString("cameraType")
|
cameraType = requestData.getString("cameraType")
|
||||||
)
|
)
|
||||||
Logger.d("Camera request received: $request")
|
|
||||||
_cameraRequest.value = request
|
Logger.step("CAMERA_REQUEST_RECEIVED",
|
||||||
|
"Camera request from operator ${cameraRequest.operatorId} for ${cameraRequest.cameraType}")
|
||||||
|
|
||||||
|
_cameraRequests.value = cameraRequest
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e("Error parsing camera request", e)
|
Logger.error("CAMERA_REQUEST_PARSE_ERROR", "Failed to parse camera request", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Constants.SocketEvents.CAMERA_DISCONNECT) { args ->
|
// Завершение сессии
|
||||||
|
on("camera:disconnect") { args ->
|
||||||
try {
|
try {
|
||||||
val data = JSONObject(args[0].toString())
|
val data = args[0] as JSONObject
|
||||||
val sessionId = data.getString("sessionId")
|
val sessionId = data.getString("sessionId")
|
||||||
Logger.d("Camera disconnect received for session: $sessionId")
|
Logger.step("CAMERA_DISCONNECT", "Session $sessionId ended by operator")
|
||||||
_sessionDisconnect.value = sessionId
|
|
||||||
|
// Очистить текущий запрос если он совпадает
|
||||||
|
if (_cameraRequests.value?.sessionId == sessionId) {
|
||||||
|
_cameraRequests.value = null
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e("Error parsing camera disconnect", e)
|
Logger.error("CAMERA_DISCONNECT_PARSE_ERROR", "Failed to parse disconnect event", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Constants.SocketEvents.CAMERA_SWITCH) { args ->
|
// WebRTC offer от оператора
|
||||||
|
on("webrtc:offer") { args ->
|
||||||
try {
|
try {
|
||||||
val data = JSONObject(args[0].toString())
|
val data = args[0] as JSONObject
|
||||||
val sessionId = data.getString("sessionId")
|
val event = WebRTCEvent.Offer(
|
||||||
val newCameraType = data.getString("newCameraType")
|
|
||||||
Logger.d("Camera switch request: $sessionId -> $newCameraType")
|
|
||||||
_cameraSwitchRequest.value = Pair(sessionId, newCameraType)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error parsing camera switch", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on(Constants.SocketEvents.WEBRTC_OFFER) { args ->
|
|
||||||
try {
|
|
||||||
val data = JSONObject(args[0].toString())
|
|
||||||
val message = WebRTCMessage(
|
|
||||||
sessionId = data.getString("sessionId"),
|
sessionId = data.getString("sessionId"),
|
||||||
type = "offer",
|
offer = data.getString("offer")
|
||||||
sdp = data.getString("offer")
|
|
||||||
)
|
)
|
||||||
Logger.d("WebRTC offer received for session: ${message.sessionId}")
|
|
||||||
_webrtcOffer.value = message
|
Logger.step("WEBRTC_OFFER_RECEIVED", "WebRTC offer received for session ${event.sessionId}")
|
||||||
|
_webRTCEvents.value = event
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e("Error parsing WebRTC offer", e)
|
Logger.error("WEBRTC_OFFER_PARSE_ERROR", "Failed to parse WebRTC offer", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on(Constants.SocketEvents.WEBRTC_ICE_CANDIDATE) { args ->
|
// WebRTC answer от оператора
|
||||||
|
on("webrtc:answer") { args ->
|
||||||
try {
|
try {
|
||||||
val data = JSONObject(args[0].toString())
|
val data = args[0] as JSONObject
|
||||||
val message = WebRTCMessage(
|
val event = WebRTCEvent.Answer(
|
||||||
|
sessionId = data.getString("sessionId"),
|
||||||
|
answer = data.getString("answer")
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_ANSWER_RECEIVED", "WebRTC answer received for session ${event.sessionId}")
|
||||||
|
_webRTCEvents.value = event
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_ANSWER_PARSE_ERROR", "Failed to parse WebRTC answer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICE кандидаты
|
||||||
|
on("webrtc:ice-candidate") { args ->
|
||||||
|
try {
|
||||||
|
val data = args[0] as JSONObject
|
||||||
|
val event = WebRTCEvent.IceCandidate(
|
||||||
sessionId = data.getString("sessionId"),
|
sessionId = data.getString("sessionId"),
|
||||||
type = "ice-candidate",
|
|
||||||
candidate = data.getString("candidate"),
|
candidate = data.getString("candidate"),
|
||||||
sdpMid = data.getString("sdpMid"),
|
sdpMid = data.getString("sdpMid"),
|
||||||
sdpMLineIndex = data.getInt("sdpMLineIndex")
|
sdpMLineIndex = data.getInt("sdpMLineIndex")
|
||||||
)
|
)
|
||||||
Logger.d("WebRTC ICE candidate received for session: ${message.sessionId}")
|
|
||||||
_webrtcIceCandidate.value = message
|
Logger.step("WEBRTC_ICE_RECEIVED", "ICE candidate received for session ${event.sessionId}")
|
||||||
|
_webRTCEvents.value = event
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e("Error parsing WebRTC ICE candidate", e)
|
Logger.error("WEBRTC_ICE_PARSE_ERROR", "Failed to parse ICE candidate", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Переключение камеры
|
||||||
|
on("camera:switch") { args ->
|
||||||
|
try {
|
||||||
|
val data = args[0] as JSONObject
|
||||||
|
val sessionId = data.getString("sessionId")
|
||||||
|
val cameraType = data.getString("cameraType")
|
||||||
|
|
||||||
|
Logger.step("CAMERA_SWITCH_REQUEST", "Camera switch request: $cameraType for session $sessionId")
|
||||||
|
|
||||||
|
// Отправить событие переключения камеры
|
||||||
|
val event = WebRTCEvent.SwitchCamera(sessionId, cameraType)
|
||||||
|
_webRTCEvents.value = event
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_SWITCH_PARSE_ERROR", "Failed to parse camera switch request", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ошибки соединения
|
||||||
|
on(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||||
|
val error = if (args.isNotEmpty()) args[0].toString() else "Unknown error"
|
||||||
|
Logger.error("SOCKET_CONNECT_ERROR", "Connection error: $error", null)
|
||||||
|
_connectionState.value = ConnectionState.ERROR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Зарегистрировать устройство на сервере
|
* Регистрация Android устройства на сервере
|
||||||
*/
|
*/
|
||||||
private fun registerDevice() {
|
private fun registerDevice() {
|
||||||
try {
|
val deviceInfo = DeviceInfo(
|
||||||
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
|
model = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
|
||||||
val deviceInfo = DeviceInfo(
|
androidVersion = android.os.Build.VERSION.RELEASE,
|
||||||
availableCameras = cameraManager.getAvailableCameraTypes()
|
appVersion = "1.0.0",
|
||||||
)
|
availableCameras = listOf("back", "front", "ultra_wide", "telephoto") // Получить из CameraManager
|
||||||
|
|
||||||
val registrationData = JsonObject().apply {
|
|
||||||
addProperty("deviceId", _deviceId.value)
|
|
||||||
add("deviceInfo", gson.toJsonTree(deviceInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
socket?.emit(Constants.SocketEvents.REGISTER_ANDROID, registrationData)
|
|
||||||
Logger.d("Device registration sent: $registrationData")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error registering device", e)
|
|
||||||
_error.value = AppError.SocketError("Failed to register device: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отправить ответ на запрос камеры
|
|
||||||
*/
|
|
||||||
fun sendCameraResponse(sessionId: String, accepted: Boolean, reason: String? = null) {
|
|
||||||
try {
|
|
||||||
val response = JsonObject().apply {
|
|
||||||
addProperty("sessionId", sessionId)
|
|
||||||
addProperty("accepted", accepted)
|
|
||||||
reason?.let { addProperty("reason", it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
socket?.emit(Constants.SocketEvents.CAMERA_RESPONSE, response)
|
|
||||||
Logger.d("Camera response sent: $response")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error sending camera response", e)
|
|
||||||
_error.value = AppError.SocketError("Failed to send camera response: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отправить WebRTC answer
|
|
||||||
*/
|
|
||||||
fun sendWebRTCAnswer(sessionId: String, answer: String) {
|
|
||||||
try {
|
|
||||||
val data = JsonObject().apply {
|
|
||||||
addProperty("sessionId", sessionId)
|
|
||||||
addProperty("answer", answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket?.emit(Constants.SocketEvents.WEBRTC_ANSWER, data)
|
|
||||||
Logger.d("WebRTC answer sent for session: $sessionId")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error sending WebRTC answer", e)
|
|
||||||
_error.value = AppError.SocketError("Failed to send WebRTC answer: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отправить ICE candidate
|
|
||||||
*/
|
|
||||||
fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) {
|
|
||||||
try {
|
|
||||||
val data = JsonObject().apply {
|
|
||||||
addProperty("sessionId", sessionId)
|
|
||||||
addProperty("candidate", candidate)
|
|
||||||
addProperty("sdpMid", sdpMid)
|
|
||||||
addProperty("sdpMLineIndex", sdpMLineIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket?.emit(Constants.SocketEvents.WEBRTC_ICE_CANDIDATE, data)
|
|
||||||
Logger.d("ICE candidate sent for session: $sessionId")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error sending ICE candidate", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать канал уведомлений
|
|
||||||
*/
|
|
||||||
private fun createNotificationChannel() {
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
Constants.NOTIFICATION_CHANNEL_ID,
|
|
||||||
"GodEye Service",
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
description = "Уведомления о состоянии подключения GodEye"
|
|
||||||
setShowBadge(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Создать уведомление для foreground service
|
|
||||||
*/
|
|
||||||
private fun createNotification(): Notification {
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val statusText = when (_connectionState.value) {
|
val registerData = JSONObject().apply {
|
||||||
ConnectionState.CONNECTED -> "Подключено"
|
put("deviceId", _deviceId.value)
|
||||||
ConnectionState.CONNECTING -> "Подключение..."
|
put("deviceInfo", JSONObject().apply {
|
||||||
ConnectionState.RECONNECTING -> "Переподключение..."
|
put("model", deviceInfo.model)
|
||||||
ConnectionState.DISCONNECTED -> "Отключено"
|
put("androidVersion", deviceInfo.androidVersion)
|
||||||
ConnectionState.ERROR -> "Ошибка подключения"
|
put("appVersion", deviceInfo.appVersion)
|
||||||
|
put("availableCameras", JSONArray().apply {
|
||||||
|
deviceInfo.availableCameras.forEach { put(it) }
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
|
socket?.emit("register:android", registerData)
|
||||||
.setContentTitle("GodEye Signal Center")
|
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.model}")
|
||||||
.setContentText("Статус: $statusText")
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправка ответа на запрос камеры
|
||||||
|
*/
|
||||||
|
fun sendCameraResponse(sessionId: String, accepted: Boolean, reason: String = "") {
|
||||||
|
val responseData = JSONObject().apply {
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("accepted", accepted)
|
||||||
|
if (!accepted && reason.isNotEmpty()) {
|
||||||
|
put("reason", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket?.emit("camera:response", responseData)
|
||||||
|
Logger.step("CAMERA_RESPONSE_SENT", "Camera response sent: sessionId=$sessionId, accepted=$accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправка WebRTC offer
|
||||||
|
*/
|
||||||
|
fun sendWebRTCOffer(sessionId: String, offer: String) {
|
||||||
|
val offerData = JSONObject().apply {
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("offer", offer)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket?.emit("webrtc:offer", offerData)
|
||||||
|
Logger.step("WEBRTC_OFFER_SENT", "WebRTC offer sent for session $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправка WebRTC answer
|
||||||
|
*/
|
||||||
|
fun sendWebRTCAnswer(sessionId: String, answer: String) {
|
||||||
|
val answerData = JSONObject().apply {
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("answer", answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket?.emit("webrtc:answer", answerData)
|
||||||
|
Logger.step("WEBRTC_ANSWER_SENT", "WebRTC answer sent for session $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправка ICE кандидата
|
||||||
|
*/
|
||||||
|
fun sendIceCandidate(sessionId: String, candidate: String, sdpMid: String, sdpMLineIndex: Int) {
|
||||||
|
val candidateData = JSONObject().apply {
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("candidate", candidate)
|
||||||
|
put("sdpMid", sdpMid)
|
||||||
|
put("sdpMLineIndex", sdpMLineIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket?.emit("webrtc:ice-candidate", candidateData)
|
||||||
|
Logger.step("WEBRTC_ICE_SENT", "ICE candidate sent for session $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отключение от сервера
|
||||||
|
*/
|
||||||
|
fun disconnect() {
|
||||||
|
Logger.step("SOCKET_DISCONNECT", "Disconnecting from server")
|
||||||
|
socket?.disconnect()
|
||||||
|
socket = null
|
||||||
|
_connectionState.value = ConnectionState.DISCONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"GodEye Service",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
description = "Сервис подключения к серверу"
|
||||||
|
setShowBadge(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("GodEye")
|
||||||
|
.setContentText("Подключено к серверу")
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Обновить уведомление
|
|
||||||
*/
|
|
||||||
private fun updateNotification() {
|
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
||||||
notificationManager.notify(Constants.FOREGROUND_SERVICE_ID, createNotification())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить события
|
|
||||||
*/
|
|
||||||
fun clearCameraRequest() {
|
|
||||||
_cameraRequest.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearWebRTCOffer() {
|
|
||||||
_webrtcOffer.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearError() {
|
|
||||||
_error.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
Logger.step("SOCKET_SERVICE_DESTROY", "SocketService destroyed")
|
||||||
disconnect()
|
disconnect()
|
||||||
Logger.d("SocketService destroyed")
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANNEL_ID = "godeye_service_channel"
|
||||||
|
private const val NOTIFICATION_ID = 1001
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* События WebRTC для обработки в UI
|
||||||
|
*/
|
||||||
|
sealed class WebRTCEvent {
|
||||||
|
data class Offer(val sessionId: String, val offer: String) : WebRTCEvent()
|
||||||
|
data class Answer(val sessionId: String, val answer: String) : WebRTCEvent()
|
||||||
|
data class IceCandidate(
|
||||||
|
val sessionId: String,
|
||||||
|
val candidate: String,
|
||||||
|
val sdpMid: String,
|
||||||
|
val sdpMLineIndex: Int
|
||||||
|
) : WebRTCEvent()
|
||||||
|
data class SwitchCamera(val sessionId: String, val cameraType: String) : WebRTCEvent()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package com.example.godeye.streaming
|
||||||
|
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import java.io.*
|
||||||
|
import java.net.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HLS Stream Manager - создает HTTP Live Streaming сервер на Android устройстве
|
||||||
|
* Позволяет операторам подключаться через http://device_ip:8080/hls/stream.m3u8
|
||||||
|
*/
|
||||||
|
class HLSStreamManager {
|
||||||
|
|
||||||
|
private var httpServer: ServerSocket? = null
|
||||||
|
private var isServerRunning = AtomicBoolean(false)
|
||||||
|
private var serverThread: Thread? = null
|
||||||
|
|
||||||
|
private val serverPort = 8080
|
||||||
|
private var deviceIP: String? = null
|
||||||
|
private val segmentQueue = ConcurrentLinkedQueue<String>()
|
||||||
|
private var segmentCounter = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
detectDeviceIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectDeviceIP() {
|
||||||
|
try {
|
||||||
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
val networkInterface = interfaces.nextElement()
|
||||||
|
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||||
|
val addresses = networkInterface.inetAddresses
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
val address = addresses.nextElement()
|
||||||
|
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||||
|
deviceIP = address.hostAddress
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_IP_DETECTION", "Failed to detect IP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startStreaming(cameraType: String = "back"): String? {
|
||||||
|
Logger.step("HLS_START_STREAMING", "🎬 Starting HLS server on port $serverPort")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isServerRunning.get()) {
|
||||||
|
Logger.step("HLS_ALREADY_RUNNING", "HLS server already running")
|
||||||
|
return "http://$deviceIP:$serverPort/hls/stream.m3u8"
|
||||||
|
}
|
||||||
|
|
||||||
|
httpServer = ServerSocket(serverPort)
|
||||||
|
isServerRunning.set(true)
|
||||||
|
|
||||||
|
serverThread = Thread {
|
||||||
|
while (isServerRunning.get()) {
|
||||||
|
try {
|
||||||
|
val clientSocket = httpServer?.accept()
|
||||||
|
if (clientSocket != null) {
|
||||||
|
handleHTTPClient(clientSocket)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (isServerRunning.get()) {
|
||||||
|
Logger.error("HLS_CLIENT_ERROR", "Error handling HTTP client", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverThread?.start()
|
||||||
|
|
||||||
|
startSegmentGeneration()
|
||||||
|
|
||||||
|
val hlsUrl = "http://$deviceIP:$serverPort/hls/stream.m3u8"
|
||||||
|
Logger.step("HLS_SERVER_STARTED", "✅ HLS server started: $hlsUrl")
|
||||||
|
return hlsUrl
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_START_ERROR", "Failed to start HLS server", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleHTTPClient(clientSocket: Socket) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
|
||||||
|
val output = PrintWriter(clientSocket.getOutputStream(), true)
|
||||||
|
|
||||||
|
val requestLine = input.readLine()
|
||||||
|
Logger.step("HLS_REQUEST", "📡 HTTP request: $requestLine")
|
||||||
|
|
||||||
|
// Читаем остальные заголовки
|
||||||
|
var line: String?
|
||||||
|
while (input.readLine().also { line = it } != null && line!!.isNotEmpty()) {
|
||||||
|
// Пропускаем заголовки
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
requestLine.contains("GET /hls/stream.m3u8") -> {
|
||||||
|
sendM3U8Playlist(output)
|
||||||
|
}
|
||||||
|
requestLine.contains("GET /hls/segment") -> {
|
||||||
|
val segmentNumber = extractSegmentNumber(requestLine)
|
||||||
|
sendSegment(output, segmentNumber)
|
||||||
|
}
|
||||||
|
requestLine.contains("GET /") -> {
|
||||||
|
sendCORSHeaders(output)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
send404(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_CLIENT_HANDLER", "Error handling HTTP client", e)
|
||||||
|
} finally {
|
||||||
|
clientSocket.close()
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendM3U8Playlist(output: PrintWriter) {
|
||||||
|
val playlist = generateM3U8Playlist()
|
||||||
|
|
||||||
|
output.println("HTTP/1.1 200 OK")
|
||||||
|
output.println("Content-Type: application/vnd.apple.mpegurl")
|
||||||
|
output.println("Content-Length: ${playlist.length}")
|
||||||
|
output.println("Access-Control-Allow-Origin: *")
|
||||||
|
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
|
||||||
|
output.println("Access-Control-Allow-Headers: Content-Type")
|
||||||
|
output.println("Cache-Control: no-cache")
|
||||||
|
output.println()
|
||||||
|
output.print(playlist)
|
||||||
|
output.flush()
|
||||||
|
|
||||||
|
Logger.step("HLS_PLAYLIST_SENT", "📋 M3U8 playlist sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateM3U8Playlist(): String {
|
||||||
|
val playlist = StringBuilder()
|
||||||
|
playlist.append("#EXTM3U\n")
|
||||||
|
playlist.append("#EXT-X-VERSION:3\n")
|
||||||
|
playlist.append("#EXT-X-TARGETDURATION:10\n")
|
||||||
|
playlist.append("#EXT-X-MEDIA-SEQUENCE:$segmentCounter\n")
|
||||||
|
playlist.append("#EXT-X-PLAYLIST-TYPE:EVENT\n")
|
||||||
|
|
||||||
|
// Добавляем последние сегменты
|
||||||
|
val segments = segmentQueue.toList().takeLast(5)
|
||||||
|
segments.forEach { segment ->
|
||||||
|
playlist.append("#EXTINF:10.0,\n")
|
||||||
|
playlist.append("$segment\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlist.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendSegment(output: PrintWriter, segmentNumber: Int) {
|
||||||
|
// В реальной реализации здесь будет отправка H.264/MPEG-TS сегмента
|
||||||
|
val segmentData = generateDummySegment(segmentNumber)
|
||||||
|
|
||||||
|
output.println("HTTP/1.1 200 OK")
|
||||||
|
output.println("Content-Type: video/mp2t")
|
||||||
|
output.println("Content-Length: ${segmentData.size}")
|
||||||
|
output.println("Access-Control-Allow-Origin: *")
|
||||||
|
output.println()
|
||||||
|
output.flush()
|
||||||
|
|
||||||
|
// Отправляем бинарные данные через OutputStream сокета
|
||||||
|
val clientSocket = (output as? PrintWriter)?.let {
|
||||||
|
// Получаем сокет из контекста (нужно передать его в метод)
|
||||||
|
null // Временное решение
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("HLS_SEGMENT_SENT", "🎥 Segment $segmentNumber sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateDummySegment(segmentNumber: Int): ByteArray {
|
||||||
|
// Заглушка - в реальной реализации здесь будут закодированные кадры
|
||||||
|
return "DUMMY_TS_SEGMENT_$segmentNumber".toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractSegmentNumber(requestLine: String): Int {
|
||||||
|
return try {
|
||||||
|
val regex = "segment(\\d+)\\.ts".toRegex()
|
||||||
|
val match = regex.find(requestLine)
|
||||||
|
match?.groupValues?.get(1)?.toInt() ?: 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendCORSHeaders(output: PrintWriter) {
|
||||||
|
output.println("HTTP/1.1 200 OK")
|
||||||
|
output.println("Access-Control-Allow-Origin: *")
|
||||||
|
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
|
||||||
|
output.println("Access-Control-Allow-Headers: Content-Type")
|
||||||
|
output.println("Content-Length: 0")
|
||||||
|
output.println()
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun send404(output: PrintWriter) {
|
||||||
|
output.println("HTTP/1.1 404 Not Found")
|
||||||
|
output.println("Content-Length: 0")
|
||||||
|
output.println()
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startSegmentGeneration() {
|
||||||
|
Thread {
|
||||||
|
Logger.step("HLS_SEGMENT_GENERATION", "🎬 Starting HLS segment generation")
|
||||||
|
|
||||||
|
while (isServerRunning.get()) {
|
||||||
|
try {
|
||||||
|
// Генерируем новый сегмент каждые 10 секунд
|
||||||
|
val segmentName = "segment${segmentCounter++}.ts"
|
||||||
|
segmentQueue.offer(segmentName)
|
||||||
|
|
||||||
|
// Ограничиваем количество сегментов
|
||||||
|
while (segmentQueue.size > 10) {
|
||||||
|
segmentQueue.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("HLS_SEGMENT_GENERATED", "📹 Generated segment: $segmentName")
|
||||||
|
Thread.sleep(10000) // 10 секунд на сегмент
|
||||||
|
|
||||||
|
} catch (_: InterruptedException) {
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_SEGMENT_ERROR", "Error generating segment", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("HLS_SWITCH_CAMERA", "🔄 Switching HLS camera to: $cameraType")
|
||||||
|
// В реальной реализации здесь будет переключение источника кадров
|
||||||
|
// TODO: Implement camera switching logic
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopStreaming() {
|
||||||
|
Logger.step("HLS_STOP_STREAMING", "🛑 Stopping HLS streaming")
|
||||||
|
|
||||||
|
try {
|
||||||
|
isServerRunning.set(false)
|
||||||
|
httpServer?.close()
|
||||||
|
httpServer = null
|
||||||
|
|
||||||
|
serverThread?.interrupt()
|
||||||
|
serverThread = null
|
||||||
|
|
||||||
|
segmentQueue.clear()
|
||||||
|
segmentCounter = 0
|
||||||
|
|
||||||
|
Logger.step("HLS_STREAMING_STOPPED", "✅ HLS streaming stopped")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_STOP_ERROR", "Error stopping HLS streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
stopStreaming()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package com.example.godeye.streaming
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.camera2.*
|
||||||
|
import android.media.MediaRecorder
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import java.io.*
|
||||||
|
import java.net.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RTSP Stream Manager - создает RTSP сервер на Android устройстве
|
||||||
|
* Позволяет операторам подключаться напрямую через rtsp://device_ip:8554/live
|
||||||
|
*/
|
||||||
|
class RTSPStreamManager(private val context: Context) {
|
||||||
|
|
||||||
|
private var serverSocket: ServerSocket? = null
|
||||||
|
private var isServerRunning = AtomicBoolean(false)
|
||||||
|
private var serverThread: Thread? = null
|
||||||
|
private val clientSockets = mutableListOf<Socket>()
|
||||||
|
|
||||||
|
private var cameraDevice: CameraDevice? = null
|
||||||
|
private var captureSession: CameraCaptureSession? = null
|
||||||
|
private var backgroundThread: HandlerThread? = null
|
||||||
|
private var backgroundHandler: Handler? = null
|
||||||
|
|
||||||
|
private val serverPort = 8554
|
||||||
|
private var deviceIP: String? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
detectDeviceIP()
|
||||||
|
startBackgroundThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectDeviceIP() {
|
||||||
|
try {
|
||||||
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
val networkInterface = interfaces.nextElement()
|
||||||
|
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||||
|
val addresses = networkInterface.inetAddresses
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
val address = addresses.nextElement()
|
||||||
|
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||||
|
deviceIP = address.hostAddress
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_IP_DETECTION", "Failed to detect IP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBackgroundThread() {
|
||||||
|
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
|
||||||
|
backgroundHandler = Handler(backgroundThread?.looper!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startServer(cameraType: String = "back"): String? {
|
||||||
|
Logger.step("RTSP_START_SERVER", "🎬 Starting RTSP server on port $serverPort")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isServerRunning.get()) {
|
||||||
|
Logger.step("RTSP_ALREADY_RUNNING", "RTSP server already running")
|
||||||
|
return "rtsp://$deviceIP:$serverPort/live"
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSocket = ServerSocket(serverPort)
|
||||||
|
isServerRunning.set(true)
|
||||||
|
|
||||||
|
serverThread = Thread {
|
||||||
|
while (isServerRunning.get()) {
|
||||||
|
try {
|
||||||
|
val clientSocket = serverSocket?.accept()
|
||||||
|
if (clientSocket != null) {
|
||||||
|
clientSockets.add(clientSocket)
|
||||||
|
handleRTSPClient(clientSocket)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (isServerRunning.get()) {
|
||||||
|
Logger.error("RTSP_CLIENT_ERROR", "Error handling client", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverThread?.start()
|
||||||
|
|
||||||
|
initializeCamera(cameraType)
|
||||||
|
|
||||||
|
val rtspUrl = "rtsp://$deviceIP:$serverPort/live"
|
||||||
|
Logger.step("RTSP_SERVER_STARTED", "✅ RTSP server started: $rtspUrl")
|
||||||
|
return rtspUrl
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_START_ERROR", "Failed to start RTSP server", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRTSPClient(clientSocket: Socket) {
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
|
||||||
|
val output = PrintWriter(clientSocket.getOutputStream(), true)
|
||||||
|
|
||||||
|
var line: String?
|
||||||
|
val request = StringBuilder()
|
||||||
|
|
||||||
|
// Читаем RTSP запрос
|
||||||
|
while (input.readLine().also { line = it } != null) {
|
||||||
|
request.append(line).append("\n")
|
||||||
|
if (line!!.isEmpty()) break
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestStr = request.toString()
|
||||||
|
Logger.step("RTSP_REQUEST", "📡 RTSP request: ${requestStr.lines().firstOrNull()}")
|
||||||
|
|
||||||
|
when {
|
||||||
|
requestStr.contains("OPTIONS") -> {
|
||||||
|
sendRTSPResponse(output, "200 OK", mapOf(
|
||||||
|
"Public" to "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
requestStr.contains("DESCRIBE") -> {
|
||||||
|
val sdp = generateSDP()
|
||||||
|
sendRTSPResponse(output, "200 OK", mapOf(
|
||||||
|
"Content-Type" to "application/sdp",
|
||||||
|
"Content-Length" to sdp.length.toString()
|
||||||
|
), sdp)
|
||||||
|
}
|
||||||
|
requestStr.contains("SETUP") -> {
|
||||||
|
sendRTSPResponse(output, "200 OK", mapOf(
|
||||||
|
"Transport" to "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001",
|
||||||
|
"Session" to "12345678"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
requestStr.contains("PLAY") -> {
|
||||||
|
sendRTSPResponse(output, "200 OK", mapOf(
|
||||||
|
"Session" to "12345678"
|
||||||
|
))
|
||||||
|
startRTPStreaming(clientSocket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_CLIENT_HANDLER", "Error handling RTSP client", e)
|
||||||
|
} finally {
|
||||||
|
clientSocket.close()
|
||||||
|
clientSockets.remove(clientSocket)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRTSPResponse(output: PrintWriter, status: String, headers: Map<String, String>, body: String = "") {
|
||||||
|
output.println("RTSP/1.0 $status")
|
||||||
|
output.println("CSeq: 1")
|
||||||
|
headers.forEach { (key, value) ->
|
||||||
|
output.println("$key: $value")
|
||||||
|
}
|
||||||
|
output.println()
|
||||||
|
if (body.isNotEmpty()) {
|
||||||
|
output.print(body)
|
||||||
|
}
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateSDP(): String {
|
||||||
|
return """v=0
|
||||||
|
o=- 0 0 IN IP4 $deviceIP
|
||||||
|
s=Android Camera Stream
|
||||||
|
c=IN IP4 $deviceIP
|
||||||
|
t=0 0
|
||||||
|
m=video 9000 RTP/AVP 96
|
||||||
|
a=rtpmap:96 H264/90000
|
||||||
|
a=fmtp:96 profile-level-id=42e01e
|
||||||
|
a=control:streamid=0
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startRTPStreaming(clientSocket: Socket) {
|
||||||
|
Logger.step("RTSP_START_RTP", "🎥 Starting RTP streaming to client")
|
||||||
|
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val rtpSocket = DatagramSocket(9000)
|
||||||
|
val clientAddress = clientSocket.inetAddress
|
||||||
|
|
||||||
|
// Симуляция RTP пакетов (в реальной реализации здесь будут кадры с камеры)
|
||||||
|
var sequenceNumber = 0
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
|
||||||
|
while (isServerRunning.get() && !clientSocket.isClosed) {
|
||||||
|
// Создаем простой RTP пакет
|
||||||
|
val rtpPacket = createRTPPacket(sequenceNumber++, timestamp, "dummy_frame".toByteArray())
|
||||||
|
val packet = DatagramPacket(rtpPacket, rtpPacket.size, clientAddress, 8000)
|
||||||
|
rtpSocket.send(packet)
|
||||||
|
|
||||||
|
Thread.sleep(33) // ~30 FPS
|
||||||
|
}
|
||||||
|
|
||||||
|
rtpSocket.close()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_RTP_ERROR", "Error in RTP streaming", e)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRTPPacket(sequenceNumber: Int, timestamp: Long, payload: ByteArray): ByteArray {
|
||||||
|
val header = ByteArray(12)
|
||||||
|
|
||||||
|
// RTP Header
|
||||||
|
header[0] = 0x80.toByte() // Version 2, no padding, no extension, no CC
|
||||||
|
header[1] = 0x60.toByte() // Marker bit + Payload type (96)
|
||||||
|
|
||||||
|
// Sequence number
|
||||||
|
header[2] = (sequenceNumber shr 8).toByte()
|
||||||
|
header[3] = (sequenceNumber and 0xFF).toByte()
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
val ts = (timestamp and 0xFFFFFFFF).toInt()
|
||||||
|
header[4] = (ts shr 24).toByte()
|
||||||
|
header[5] = (ts shr 16).toByte()
|
||||||
|
header[6] = (ts shr 8).toByte()
|
||||||
|
header[7] = (ts and 0xFF).toByte()
|
||||||
|
|
||||||
|
// SSRC (synchronization source identifier)
|
||||||
|
header[8] = 0x12.toByte()
|
||||||
|
header[9] = 0x34.toByte()
|
||||||
|
header[10] = 0x56.toByte()
|
||||||
|
header[11] = 0x78.toByte()
|
||||||
|
|
||||||
|
return header + payload
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeCamera(cameraType: String) {
|
||||||
|
Logger.step("RTSP_INIT_CAMERA", "📷 Initializing camera for RTSP")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||||
|
val cameraId = if (cameraType == "front") {
|
||||||
|
cameraManager.cameraIdList.find {
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(it)
|
||||||
|
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cameraManager.cameraIdList.find {
|
||||||
|
val characteristics = cameraManager.getCameraCharacteristics(it)
|
||||||
|
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
|
||||||
|
}
|
||||||
|
} ?: cameraManager.cameraIdList.firstOrNull()
|
||||||
|
|
||||||
|
if (cameraId != null) {
|
||||||
|
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||||
|
override fun onOpened(camera: CameraDevice) {
|
||||||
|
cameraDevice = camera
|
||||||
|
Logger.step("RTSP_CAMERA_OPENED", "✅ Camera opened for RTSP")
|
||||||
|
createCaptureSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(camera: CameraDevice) {
|
||||||
|
camera.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(camera: CameraDevice, error: Int) {
|
||||||
|
Logger.error("RTSP_CAMERA_ERROR", "Camera error: $error")
|
||||||
|
camera.close()
|
||||||
|
cameraDevice = null
|
||||||
|
}
|
||||||
|
}, backgroundHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_CAMERA_INIT_ERROR", "Failed to initialize camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCaptureSession() {
|
||||||
|
// В реальной реализации здесь будет создание сессии захвата кадров
|
||||||
|
// и их кодирование в H.264 для передачи через RTP
|
||||||
|
Logger.step("RTSP_CAPTURE_SESSION", "📹 Camera capture session created")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("RTSP_SWITCH_CAMERA", "🔄 Switching RTSP camera to: $cameraType")
|
||||||
|
|
||||||
|
// Закрываем текущую камеру и открываем новую
|
||||||
|
cameraDevice?.close()
|
||||||
|
initializeCamera(cameraType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopServer() {
|
||||||
|
Logger.step("RTSP_STOP_SERVER", "🛑 Stopping RTSP server")
|
||||||
|
|
||||||
|
try {
|
||||||
|
isServerRunning.set(false)
|
||||||
|
|
||||||
|
clientSockets.forEach { it.close() }
|
||||||
|
clientSockets.clear()
|
||||||
|
|
||||||
|
serverSocket?.close()
|
||||||
|
serverSocket = null
|
||||||
|
|
||||||
|
cameraDevice?.close()
|
||||||
|
cameraDevice = null
|
||||||
|
|
||||||
|
captureSession?.close()
|
||||||
|
captureSession = null
|
||||||
|
|
||||||
|
serverThread?.interrupt()
|
||||||
|
serverThread = null
|
||||||
|
|
||||||
|
Logger.step("RTSP_SERVER_STOPPED", "✅ RTSP server stopped")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_STOP_ERROR", "Error stopping RTSP server", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
stopServer()
|
||||||
|
backgroundThread?.quitSafely()
|
||||||
|
backgroundThread = null
|
||||||
|
backgroundHandler = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package com.example.godeye.streaming
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import java.net.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UDP Stream Manager - прямая передача видео через UDP для минимальной задержки
|
||||||
|
* Позволяет операторам получать сырой видео поток через udp://device_ip:9999
|
||||||
|
*/
|
||||||
|
class UDPStreamManager(private val context: Context) {
|
||||||
|
|
||||||
|
private var udpSocket: DatagramSocket? = null
|
||||||
|
private var isStreaming = AtomicBoolean(false)
|
||||||
|
private var streamingThread: Thread? = null
|
||||||
|
|
||||||
|
private val streamingPort = 9999
|
||||||
|
private var deviceIP: String? = null
|
||||||
|
private var targetAddress: InetAddress? = null
|
||||||
|
private var targetPort: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
detectDeviceIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectDeviceIP() {
|
||||||
|
try {
|
||||||
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
val networkInterface = interfaces.nextElement()
|
||||||
|
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||||
|
val addresses = networkInterface.inetAddresses
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
val address = addresses.nextElement()
|
||||||
|
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||||
|
deviceIP = address.hostAddress
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_IP_DETECTION", "Failed to detect IP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startStreaming(cameraType: String = "back"): String? {
|
||||||
|
Logger.step("UDP_START_STREAMING", "🎬 Starting UDP streaming on port $streamingPort")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isStreaming.get()) {
|
||||||
|
Logger.step("UDP_ALREADY_STREAMING", "UDP streaming already active")
|
||||||
|
return "udp://$deviceIP:$streamingPort"
|
||||||
|
}
|
||||||
|
|
||||||
|
udpSocket = DatagramSocket(streamingPort)
|
||||||
|
isStreaming.set(true)
|
||||||
|
|
||||||
|
startFrameStreaming(cameraType)
|
||||||
|
|
||||||
|
val udpUrl = "udp://$deviceIP:$streamingPort"
|
||||||
|
Logger.step("UDP_STREAMING_STARTED", "✅ UDP streaming started: $udpUrl")
|
||||||
|
return udpUrl
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_START_ERROR", "Failed to start UDP streaming", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startFrameStreaming(cameraType: String) {
|
||||||
|
streamingThread = Thread {
|
||||||
|
Logger.step("UDP_FRAME_STREAMING", "🎥 Starting UDP frame streaming")
|
||||||
|
|
||||||
|
var frameCounter = 0
|
||||||
|
|
||||||
|
while (isStreaming.get()) {
|
||||||
|
try {
|
||||||
|
// В реальной реализации здесь будут кадры с камеры
|
||||||
|
val frameData = generateDummyFrame(frameCounter++, cameraType)
|
||||||
|
|
||||||
|
// Если есть подключенные клиенты, отправляем им кадры
|
||||||
|
if (targetAddress != null && targetPort > 0) {
|
||||||
|
sendFrame(frameData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~30 FPS
|
||||||
|
Thread.sleep(33)
|
||||||
|
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_FRAME_ERROR", "Error streaming frame", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamingThread?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateDummyFrame(frameNumber: Int, cameraType: String): ByteArray {
|
||||||
|
// Заглушка - в реальной реализации здесь будут сжатые кадры H.264
|
||||||
|
val frameHeader = ByteArray(8)
|
||||||
|
|
||||||
|
// Frame header: magic number + frame number + camera type
|
||||||
|
frameHeader[0] = 0x47.toByte() // Magic 'G'
|
||||||
|
frameHeader[1] = 0x45.toByte() // Magic 'E'
|
||||||
|
frameHeader[2] = (frameNumber shr 24).toByte()
|
||||||
|
frameHeader[3] = (frameNumber shr 16).toByte()
|
||||||
|
frameHeader[4] = (frameNumber shr 8).toByte()
|
||||||
|
frameHeader[5] = (frameNumber and 0xFF).toByte()
|
||||||
|
frameHeader[6] = if (cameraType == "back") 0x00 else 0x01
|
||||||
|
frameHeader[7] = 0x00 // Reserved
|
||||||
|
|
||||||
|
val frameData = "FRAME_${frameNumber}_${cameraType}_${System.currentTimeMillis()}".toByteArray()
|
||||||
|
return frameHeader + frameData
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendFrame(frameData: ByteArray) {
|
||||||
|
try {
|
||||||
|
val packet = DatagramPacket(
|
||||||
|
frameData,
|
||||||
|
frameData.size,
|
||||||
|
targetAddress,
|
||||||
|
targetPort
|
||||||
|
)
|
||||||
|
udpSocket?.send(packet)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_SEND_FRAME", "Error sending UDP frame", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает адрес клиента для отправки кадров
|
||||||
|
*/
|
||||||
|
fun setClient(clientIP: String, clientPort: Int) {
|
||||||
|
try {
|
||||||
|
targetAddress = InetAddress.getByName(clientIP)
|
||||||
|
targetPort = clientPort
|
||||||
|
Logger.step("UDP_CLIENT_SET", "📡 UDP client set: $clientIP:$clientPort")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_SET_CLIENT", "Error setting UDP client", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение информации о UDP стриме для отправки клиенту
|
||||||
|
*/
|
||||||
|
fun getStreamInfo(): Map<String, Any> {
|
||||||
|
return mapOf(
|
||||||
|
"protocol" to "udp",
|
||||||
|
"ip" to (deviceIP ?: "unknown"),
|
||||||
|
"port" to streamingPort,
|
||||||
|
"url" to "udp://$deviceIP:$streamingPort",
|
||||||
|
"format" to "raw_frames",
|
||||||
|
"fps" to 30,
|
||||||
|
"active" to isStreaming.get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("UDP_SWITCH_CAMERA", "🔄 Switching UDP camera to: $cameraType")
|
||||||
|
// В реальной реализации здесь будет переключение источника кадров
|
||||||
|
// Новый тип камеры будет включен в следующие кадры
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopStreaming() {
|
||||||
|
Logger.step("UDP_STOP_STREAMING", "🛑 Stopping UDP streaming")
|
||||||
|
|
||||||
|
try {
|
||||||
|
isStreaming.set(false)
|
||||||
|
|
||||||
|
streamingThread?.interrupt()
|
||||||
|
streamingThread = null
|
||||||
|
|
||||||
|
udpSocket?.close()
|
||||||
|
udpSocket = null
|
||||||
|
|
||||||
|
targetAddress = null
|
||||||
|
targetPort = 0
|
||||||
|
|
||||||
|
Logger.step("UDP_STREAMING_STOPPED", "✅ UDP streaming stopped")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_STOP_ERROR", "Error stopping UDP streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
stopStreaming()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
package com.example.godeye.streaming
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.hardware.camera2.CameraManager
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.net.SocketException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified Streaming Manager - управляет различными протоколами прямой передачи видео
|
||||||
|
*
|
||||||
|
* Поддерживаемые протоколы:
|
||||||
|
* 1. WebRTC P2P - для веб-браузеров
|
||||||
|
* 2. RTSP Server - для специализированных клиентов
|
||||||
|
* 3. HTTP Live Streaming (HLS) - для универсальной совместимости
|
||||||
|
* 4. Raw UDP Stream - для минимальной задержки
|
||||||
|
*/
|
||||||
|
class UnifiedStreamingManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Состояния стриминга
|
||||||
|
private val _streamingState = MutableStateFlow(StreamingState.STOPPED)
|
||||||
|
val streamingState: StateFlow<StreamingState> = _streamingState.asStateFlow()
|
||||||
|
|
||||||
|
private val _availableProtocols = MutableStateFlow<List<StreamingProtocol>>(emptyList())
|
||||||
|
val availableProtocols: StateFlow<List<StreamingProtocol>> = _availableProtocols.asStateFlow()
|
||||||
|
|
||||||
|
private val _activeStreams = MutableStateFlow<Map<String, StreamInfo>>(emptyMap())
|
||||||
|
val activeStreams: StateFlow<Map<String, StreamInfo>> = _activeStreams.asStateFlow()
|
||||||
|
|
||||||
|
// Менеджеры протоколов
|
||||||
|
private var webRTCManager: WebRTCStreamManager? = null
|
||||||
|
private var rtspManager: RTSPStreamManager? = null
|
||||||
|
private var hlsManager: HLSStreamManager? = null
|
||||||
|
private var udpManager: UDPStreamManager? = null
|
||||||
|
|
||||||
|
private var deviceIP: String? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
Logger.step("UNIFIED_STREAMING_INIT", "🎬 Initializing Unified Streaming Manager")
|
||||||
|
detectDeviceIP()
|
||||||
|
initializeProtocolSupport()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Определение IP адреса устройства для прямых соединений
|
||||||
|
*/
|
||||||
|
private fun detectDeviceIP() {
|
||||||
|
try {
|
||||||
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
|
while (interfaces.hasMoreElements()) {
|
||||||
|
val networkInterface = interfaces.nextElement()
|
||||||
|
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||||
|
val addresses = networkInterface.inetAddresses
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
val address = addresses.nextElement()
|
||||||
|
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||||
|
deviceIP = address.hostAddress
|
||||||
|
Logger.step("DEVICE_IP_DETECTED", "📍 Device IP: $deviceIP")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SocketException) {
|
||||||
|
Logger.error("IP_DETECTION_ERROR", "Failed to detect device IP", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация поддержки различных протоколов
|
||||||
|
*/
|
||||||
|
private fun initializeProtocolSupport() {
|
||||||
|
val supportedProtocols = mutableListOf<StreamingProtocol>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// WebRTC поддержка
|
||||||
|
webRTCManager = WebRTCStreamManager(context, onSignalingMessage)
|
||||||
|
supportedProtocols.add(
|
||||||
|
StreamingProtocol(
|
||||||
|
type = "webrtc",
|
||||||
|
name = "WebRTC P2P",
|
||||||
|
description = "Прямое P2P соединение для веб-браузеров",
|
||||||
|
isSupported = true,
|
||||||
|
connectionInfo = "Автоматическое P2P соединение"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Logger.step("WEBRTC_SUPPORT", "✅ WebRTC protocol supported")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_SUPPORT_ERROR", "WebRTC not supported", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// RTSP Server поддержка
|
||||||
|
rtspManager = RTSPStreamManager(context)
|
||||||
|
supportedProtocols.add(
|
||||||
|
StreamingProtocol(
|
||||||
|
type = "rtsp",
|
||||||
|
name = "RTSP Server",
|
||||||
|
description = "RTSP сервер для специализированных клиентов",
|
||||||
|
isSupported = true,
|
||||||
|
connectionInfo = "rtsp://$deviceIP:8554/live"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Logger.step("RTSP_SUPPORT", "✅ RTSP protocol supported")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("RTSP_SUPPORT_ERROR", "RTSP not supported", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// HLS поддержка
|
||||||
|
hlsManager = HLSStreamManager()
|
||||||
|
supportedProtocols.add(
|
||||||
|
StreamingProtocol(
|
||||||
|
type = "hls",
|
||||||
|
name = "HTTP Live Streaming",
|
||||||
|
description = "HLS стрим для универсальной совместимости",
|
||||||
|
isSupported = true,
|
||||||
|
connectionInfo = "http://$deviceIP:8080/hls/stream.m3u8"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Logger.step("HLS_SUPPORT", "✅ HLS protocol supported")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("HLS_SUPPORT_ERROR", "HLS not supported", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// UDP Raw Stream поддержка
|
||||||
|
udpManager = UDPStreamManager(context)
|
||||||
|
supportedProtocols.add(
|
||||||
|
StreamingProtocol(
|
||||||
|
type = "udp",
|
||||||
|
name = "Raw UDP Stream",
|
||||||
|
description = "Прямой UDP поток для минимальной задержки",
|
||||||
|
isSupported = true,
|
||||||
|
connectionInfo = "udp://$deviceIP:9999"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Logger.step("UDP_SUPPORT", "✅ UDP protocol supported")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("UDP_SUPPORT_ERROR", "UDP not supported", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
_availableProtocols.value = supportedProtocols
|
||||||
|
Logger.step("PROTOCOLS_INITIALIZED", "🎯 Initialized ${supportedProtocols.size} streaming protocols")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск стриминга с выбранными протоколами
|
||||||
|
*/
|
||||||
|
fun startStreaming(
|
||||||
|
sessionId: String,
|
||||||
|
requestedProtocols: List<String> = listOf("webrtc", "rtsp"),
|
||||||
|
cameraType: String = "back"
|
||||||
|
) {
|
||||||
|
Logger.step("START_STREAMING", "🎬 Starting streaming for session: $sessionId")
|
||||||
|
Logger.step("STREAMING_PROTOCOLS", "📡 Requested protocols: ${requestedProtocols.joinToString(", ")}")
|
||||||
|
|
||||||
|
_streamingState.value = StreamingState.STARTING
|
||||||
|
val activeStreams = mutableMapOf<String, StreamInfo>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Запуск WebRTC если запрошен
|
||||||
|
if ("webrtc" in requestedProtocols && webRTCManager != null) {
|
||||||
|
webRTCManager?.startStreaming(sessionId, cameraType)
|
||||||
|
activeStreams["webrtc"] = StreamInfo(
|
||||||
|
protocol = "webrtc",
|
||||||
|
sessionId = sessionId,
|
||||||
|
isActive = true,
|
||||||
|
connectionUrl = "P2P Connection",
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
Logger.step("WEBRTC_STARTED", "✅ WebRTC streaming started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск RTSP если запрошен
|
||||||
|
if ("rtsp" in requestedProtocols && rtspManager != null) {
|
||||||
|
val rtspUrl = rtspManager?.startServer(cameraType)
|
||||||
|
if (rtspUrl != null) {
|
||||||
|
activeStreams["rtsp"] = StreamInfo(
|
||||||
|
protocol = "rtsp",
|
||||||
|
sessionId = sessionId,
|
||||||
|
isActive = true,
|
||||||
|
connectionUrl = rtspUrl,
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
Logger.step("RTSP_STARTED", "✅ RTSP streaming started: $rtspUrl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск HLS если запрошен
|
||||||
|
if ("hls" in requestedProtocols && hlsManager != null) {
|
||||||
|
val hlsUrl = hlsManager?.startStreaming(cameraType)
|
||||||
|
if (hlsUrl != null) {
|
||||||
|
activeStreams["hls"] = StreamInfo(
|
||||||
|
protocol = "hls",
|
||||||
|
sessionId = sessionId,
|
||||||
|
isActive = true,
|
||||||
|
connectionUrl = hlsUrl,
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
Logger.step("HLS_STARTED", "✅ HLS streaming started: $hlsUrl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запуск UDP если запрошен
|
||||||
|
if ("udp" in requestedProtocols && udpManager != null) {
|
||||||
|
val udpUrl = udpManager?.startStreaming(cameraType)
|
||||||
|
if (udpUrl != null) {
|
||||||
|
activeStreams["udp"] = StreamInfo(
|
||||||
|
protocol = "udp",
|
||||||
|
sessionId = sessionId,
|
||||||
|
isActive = true,
|
||||||
|
connectionUrl = udpUrl,
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
Logger.step("UDP_STARTED", "✅ UDP streaming started: $udpUrl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_activeStreams.value = activeStreams
|
||||||
|
_streamingState.value = if (activeStreams.isNotEmpty()) StreamingState.ACTIVE else StreamingState.ERROR
|
||||||
|
|
||||||
|
// Отправляем информацию о доступных стримах оператору через сигнальный сервер
|
||||||
|
sendStreamingInfo(sessionId, activeStreams)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("START_STREAMING_ERROR", "Failed to start streaming", e)
|
||||||
|
_streamingState.value = StreamingState.ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановка всех стримов
|
||||||
|
*/
|
||||||
|
fun stopStreaming(sessionId: String) {
|
||||||
|
Logger.step("STOP_STREAMING", "🛑 Stopping streaming for session: $sessionId")
|
||||||
|
|
||||||
|
try {
|
||||||
|
webRTCManager?.stopStreaming()
|
||||||
|
rtspManager?.stopServer()
|
||||||
|
hlsManager?.stopStreaming()
|
||||||
|
udpManager?.stopStreaming()
|
||||||
|
|
||||||
|
_activeStreams.value = emptyMap()
|
||||||
|
_streamingState.value = StreamingState.STOPPED
|
||||||
|
|
||||||
|
Logger.step("STREAMING_STOPPED", "✅ All streaming stopped")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("STOP_STREAMING_ERROR", "Error stopping streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение камеры во всех активных стримах
|
||||||
|
*/
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("SWITCH_CAMERA", "🔄 Switching camera to: $cameraType")
|
||||||
|
|
||||||
|
try {
|
||||||
|
webRTCManager?.switchCamera(cameraType)
|
||||||
|
rtspManager?.switchCamera(cameraType)
|
||||||
|
hlsManager?.switchCamera(cameraType)
|
||||||
|
udpManager?.switchCamera(cameraType)
|
||||||
|
|
||||||
|
Logger.step("CAMERA_SWITCHED", "✅ Camera switched to: $cameraType")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("SWITCH_CAMERA_ERROR", "Error switching camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправка информации о доступных стримах оператору
|
||||||
|
*/
|
||||||
|
private fun sendStreamingInfo(sessionId: String, streams: Map<String, StreamInfo>) {
|
||||||
|
val streamingInfo = JSONObject().apply {
|
||||||
|
put("type", "streaming_info")
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("deviceIP", deviceIP)
|
||||||
|
put("streams", JSONObject().apply {
|
||||||
|
streams.forEach { (protocol, info) ->
|
||||||
|
put(protocol, JSONObject().apply {
|
||||||
|
put("url", info.connectionUrl)
|
||||||
|
put("active", info.isActive)
|
||||||
|
put("protocol", info.protocol)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSignalingMessage(streamingInfo)
|
||||||
|
Logger.step("STREAMING_INFO_SENT", "📡 Streaming info sent to operator")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение статистики стриминга
|
||||||
|
*/
|
||||||
|
fun getStreamingStats(): Map<String, Any> {
|
||||||
|
val stats = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
stats["state"] = _streamingState.value.name
|
||||||
|
stats["activeStreams"] = _activeStreams.value.size
|
||||||
|
stats["deviceIP"] = deviceIP ?: "unknown"
|
||||||
|
stats["supportedProtocols"] = _availableProtocols.value.map { it.type }
|
||||||
|
|
||||||
|
_activeStreams.value.forEach { (protocol, info) ->
|
||||||
|
stats["${protocol}_uptime"] = (System.currentTimeMillis() - info.startTime) / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
Logger.step("UNIFIED_STREAMING_DISPOSE", "🧹 Disposing Unified Streaming Manager")
|
||||||
|
|
||||||
|
try {
|
||||||
|
stopStreaming("dispose")
|
||||||
|
webRTCManager?.dispose()
|
||||||
|
rtspManager?.dispose()
|
||||||
|
hlsManager?.dispose()
|
||||||
|
udpManager?.dispose()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("DISPOSE_ERROR", "Error during disposal", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Данные классы для стриминга
|
||||||
|
enum class StreamingState {
|
||||||
|
STOPPED, STARTING, ACTIVE, ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StreamingProtocol(
|
||||||
|
val type: String,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val isSupported: Boolean,
|
||||||
|
val connectionInfo: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StreamInfo(
|
||||||
|
val protocol: String,
|
||||||
|
val sessionId: String,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val connectionUrl: String,
|
||||||
|
val startTime: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package com.example.godeye.streaming
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.webrtc.*
|
||||||
|
import org.webrtc.audio.JavaAudioDeviceModule
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebRTC Stream Manager - улучшенная версия для прямого P2P соединения
|
||||||
|
*/
|
||||||
|
class WebRTCStreamManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||||
|
private var peerConnection: PeerConnection? = null
|
||||||
|
private var localVideoTrack: VideoTrack? = null
|
||||||
|
private var localAudioTrack: AudioTrack? = null
|
||||||
|
private var videoCapturer: CameraVideoCapturer? = null
|
||||||
|
private var surfaceTextureHelper: SurfaceTextureHelper? = null
|
||||||
|
|
||||||
|
private val _connectionState = MutableStateFlow(PeerConnection.PeerConnectionState.NEW)
|
||||||
|
val connectionState: StateFlow<PeerConnection.PeerConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
|
private val _isStreaming = MutableStateFlow(false)
|
||||||
|
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
|
||||||
|
|
||||||
|
private val iceServers = listOf(
|
||||||
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
|
||||||
|
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initializePeerConnectionFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializePeerConnectionFactory() {
|
||||||
|
val initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
|
||||||
|
.setEnableInternalTracer(true)
|
||||||
|
.createInitializationOptions()
|
||||||
|
PeerConnectionFactory.initialize(initOptions)
|
||||||
|
|
||||||
|
val audioDeviceModule = JavaAudioDeviceModule.builder(context).createAudioDeviceModule()
|
||||||
|
|
||||||
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
|
.setAudioDeviceModule(audioDeviceModule)
|
||||||
|
.setVideoEncoderFactory(DefaultVideoEncoderFactory(
|
||||||
|
EglBase.create().eglBaseContext, true, true))
|
||||||
|
.setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext))
|
||||||
|
.createPeerConnectionFactory()
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_FACTORY_READY", "✅ WebRTC PeerConnectionFactory initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startStreaming(sessionId: String, cameraType: String = "back") {
|
||||||
|
Logger.step("WEBRTC_START_STREAMING", "🎬 Starting WebRTC streaming for session: $sessionId")
|
||||||
|
|
||||||
|
try {
|
||||||
|
createPeerConnection()
|
||||||
|
initializeLocalMedia(cameraType)
|
||||||
|
createOffer(sessionId)
|
||||||
|
_isStreaming.value = true
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopStreaming() {
|
||||||
|
Logger.step("WEBRTC_STOP_STREAMING", "🛑 Stopping WebRTC streaming")
|
||||||
|
|
||||||
|
try {
|
||||||
|
videoCapturer?.stopCapture()
|
||||||
|
videoCapturer?.dispose()
|
||||||
|
|
||||||
|
localVideoTrack?.dispose()
|
||||||
|
localAudioTrack?.dispose()
|
||||||
|
|
||||||
|
peerConnection?.close()
|
||||||
|
peerConnection = null
|
||||||
|
|
||||||
|
_isStreaming.value = false
|
||||||
|
_connectionState.value = PeerConnection.PeerConnectionState.CLOSED
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_STOP_ERROR", "Error stopping WebRTC", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("WEBRTC_SWITCH_CAMERA", "🔄 Switching WebRTC camera to: $cameraType")
|
||||||
|
(videoCapturer as? CameraVideoCapturer)?.switchCamera(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPeerConnection() {
|
||||||
|
val config = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||||
|
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||||
|
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection = peerConnectionFactory?.createPeerConnection(config, object : PeerConnection.Observer {
|
||||||
|
override fun onSignalingChange(state: PeerConnection.SignalingState) {
|
||||||
|
Logger.step("WEBRTC_SIGNALING", "Signaling state: $state")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {
|
||||||
|
Logger.step("WEBRTC_ICE_STATE", "ICE state: $state")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnectionChange(state: PeerConnection.PeerConnectionState) {
|
||||||
|
_connectionState.value = state
|
||||||
|
Logger.step("WEBRTC_CONNECTION_STATE", "Connection state: $state")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceCandidate(candidate: IceCandidate) {
|
||||||
|
val candidateMsg = JSONObject().apply {
|
||||||
|
put("type", "ice-candidate")
|
||||||
|
put("candidate", candidate.sdp)
|
||||||
|
put("sdpMLineIndex", candidate.sdpMLineIndex)
|
||||||
|
put("sdpMid", candidate.sdpMid)
|
||||||
|
}
|
||||||
|
onSignalingMessage(candidateMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {}
|
||||||
|
override fun onAddStream(stream: MediaStream) {}
|
||||||
|
override fun onRemoveStream(stream: MediaStream) {}
|
||||||
|
override fun onDataChannel(channel: DataChannel) {}
|
||||||
|
override fun onRenegotiationNeeded() {}
|
||||||
|
override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {}
|
||||||
|
override fun onIceConnectionReceivingChange(receiving: Boolean) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeLocalMedia(cameraType: String) {
|
||||||
|
val videoSource = peerConnectionFactory?.createVideoSource(false)
|
||||||
|
localVideoTrack = peerConnectionFactory?.createVideoTrack("video", videoSource)
|
||||||
|
|
||||||
|
val audioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
|
||||||
|
localAudioTrack = peerConnectionFactory?.createAudioTrack("audio", audioSource)
|
||||||
|
|
||||||
|
val stream = peerConnectionFactory?.createLocalMediaStream("stream")
|
||||||
|
localVideoTrack?.let { stream?.addTrack(it) }
|
||||||
|
localAudioTrack?.let { stream?.addTrack(it) }
|
||||||
|
|
||||||
|
stream?.let { peerConnection?.addStream(it) }
|
||||||
|
|
||||||
|
initializeCamera(videoSource, cameraType)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeCamera(videoSource: VideoSource?, cameraType: String) {
|
||||||
|
val cameraEnumerator = Camera2Enumerator(context)
|
||||||
|
val cameraName = if (cameraType == "front") {
|
||||||
|
cameraEnumerator.deviceNames.find { cameraEnumerator.isFrontFacing(it) }
|
||||||
|
} else {
|
||||||
|
cameraEnumerator.deviceNames.find { cameraEnumerator.isBackFacing(it) }
|
||||||
|
} ?: cameraEnumerator.deviceNames.firstOrNull()
|
||||||
|
|
||||||
|
if (cameraName != null) {
|
||||||
|
surfaceTextureHelper = SurfaceTextureHelper.create("CameraThread", EglBase.create().eglBaseContext)
|
||||||
|
videoCapturer = cameraEnumerator.createCapturer(cameraName, null) as? CameraVideoCapturer
|
||||||
|
|
||||||
|
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource?.capturerObserver)
|
||||||
|
videoCapturer?.startCapture(1280, 720, 30)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOffer(sessionId: String) {
|
||||||
|
val constraints = MediaConstraints()
|
||||||
|
peerConnection?.createOffer(object : SdpObserver {
|
||||||
|
override fun onCreateSuccess(desc: SessionDescription) {
|
||||||
|
peerConnection?.setLocalDescription(object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
val offerMsg = JSONObject().apply {
|
||||||
|
put("type", "offer")
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("sdp", desc.description)
|
||||||
|
}
|
||||||
|
onSignalingMessage(offerMsg)
|
||||||
|
}
|
||||||
|
override fun onSetFailure(error: String) {}
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
}, desc)
|
||||||
|
}
|
||||||
|
override fun onCreateFailure(error: String) {}
|
||||||
|
override fun onSetSuccess() {}
|
||||||
|
override fun onSetFailure(error: String) {}
|
||||||
|
}, constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAnswer(answerSdp: String) {
|
||||||
|
val desc = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
|
||||||
|
peerConnection?.setRemoteDescription(object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
Logger.step("WEBRTC_ANSWER_SET", "✅ WebRTC answer processed")
|
||||||
|
}
|
||||||
|
override fun onSetFailure(error: String) {}
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
}, desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleIceCandidate(candidate: String, sdpMLineIndex: Int, sdpMid: String) {
|
||||||
|
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidate)
|
||||||
|
peerConnection?.addIceCandidate(iceCandidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
stopStreaming()
|
||||||
|
peerConnectionFactory?.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.example.godeye.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedFloatingPanel(
|
||||||
|
visible: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
slideDirection: SlideDirection = SlideDirection.FromBottom,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = slideInVertically(
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
) { fullHeight ->
|
||||||
|
when (slideDirection) {
|
||||||
|
SlideDirection.FromTop -> -fullHeight
|
||||||
|
SlideDirection.FromBottom -> fullHeight
|
||||||
|
}
|
||||||
|
} + fadeIn(animationSpec = tween(300)),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
|
stiffness = Spring.StiffnessMedium
|
||||||
|
)
|
||||||
|
) { fullHeight ->
|
||||||
|
when (slideDirection) {
|
||||||
|
SlideDirection.FromTop -> -fullHeight
|
||||||
|
SlideDirection.FromBottom -> fullHeight
|
||||||
|
}
|
||||||
|
} + fadeOut(animationSpec = tween(200)),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PulsingRecordIndicator(
|
||||||
|
isRecording: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (isRecording) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "recording")
|
||||||
|
val scale by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0.8f,
|
||||||
|
targetValue = 1.2f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.scale(scale)
|
||||||
|
.background(
|
||||||
|
GodEyeColors.RecordRed,
|
||||||
|
RoundedCornerShape(6.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "REC",
|
||||||
|
color = GodEyeColors.RecordRed,
|
||||||
|
style = MaterialTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedControlButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isActive: Boolean = false,
|
||||||
|
activeColor: Color = GodEyeColors.NavyLight,
|
||||||
|
size: ButtonSize = ButtonSize.Medium
|
||||||
|
) {
|
||||||
|
val scale by animateFloatAsState(
|
||||||
|
targetValue = if (isActive) 1.1f else 1.0f,
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
|
||||||
|
label = "button_scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = if (isActive) activeColor else GodEyeColors.BlackSoft.copy(alpha = 0.8f),
|
||||||
|
animationSpec = tween(300),
|
||||||
|
label = "button_color"
|
||||||
|
)
|
||||||
|
|
||||||
|
val buttonSize = when (size) {
|
||||||
|
ButtonSize.Small -> 40.dp
|
||||||
|
ButtonSize.Medium -> 56.dp
|
||||||
|
ButtonSize.Large -> 72.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
.size(buttonSize)
|
||||||
|
.scale(scale)
|
||||||
|
.background(
|
||||||
|
backgroundColor,
|
||||||
|
RoundedCornerShape(buttonSize / 2)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = if (isActive) Color.White else GodEyeColors.IvoryPure,
|
||||||
|
modifier = Modifier.size(buttonSize * 0.4f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SlideInErrorSnackbar(
|
||||||
|
error: String?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = error != null,
|
||||||
|
enter = slideInVertically(
|
||||||
|
initialOffsetY = { -it },
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = slideOutVertically(
|
||||||
|
targetOffsetY = { -it },
|
||||||
|
animationSpec = tween(300)
|
||||||
|
) + fadeOut(),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
if (error != null) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = "Закрыть",
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(error) {
|
||||||
|
kotlinx.coroutines.delay(5000)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SlideDirection {
|
||||||
|
FromTop,
|
||||||
|
FromBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ButtonSize {
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Large
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package com.example.godeye.ui.components
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import com.example.godeye.R
|
|
||||||
import com.example.godeye.models.CameraRequest
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Диалог запроса доступа к камере от оператора
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CameraRequestDialog(
|
|
||||||
request: CameraRequest,
|
|
||||||
onAccept: () -> Unit,
|
|
||||||
onDeny: () -> Unit,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
var rememberChoice by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Лог открытия диалога
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
Log.d("GodEye", "CameraRequestDialog открыт: sessionId=${request.sessionId}, operatorId=${request.operatorId}, cameraType=${request.cameraType}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = {
|
|
||||||
Log.d("GodEye", "Диалог закрыт пользователем")
|
|
||||||
onDismiss()
|
|
||||||
}) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// Иконка камеры
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Person, // Заменено с PhotoCamera на Person
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(48.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Заголовок
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.camera_request_title),
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// Основное сообщение
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
R.string.camera_request_message,
|
|
||||||
request.operatorId,
|
|
||||||
getCameraTypeName(request.cameraType)
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// ID сессии
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.session_id_label, request.sessionId),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Чекбокс "Запомнить выбор"
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = rememberChoice,
|
|
||||||
onCheckedChange = { rememberChoice = it }
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.remember_choice),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
// Кнопки
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
// Кнопка "Отклонить"
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = {
|
|
||||||
Log.d("GodEye", "Пользователь отклонил запрос камеры")
|
|
||||||
onDeny()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(48.dp)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.deny_button))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Кнопка "Разрешить"
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
Log.d("GodEye", "Пользователь разрешил доступ к камере")
|
|
||||||
onAccept()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.height(48.dp)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.allow_button))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить локализованное название типа камеры
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun getCameraTypeName(cameraType: String): String {
|
|
||||||
return when (cameraType) {
|
|
||||||
Constants.CameraTypes.BACK -> stringResource(R.string.camera_type_back)
|
|
||||||
Constants.CameraTypes.FRONT -> stringResource(R.string.camera_type_front)
|
|
||||||
Constants.CameraTypes.WIDE -> stringResource(R.string.camera_type_wide)
|
|
||||||
Constants.CameraTypes.TELEPHOTO -> stringResource(R.string.camera_type_telephoto)
|
|
||||||
else -> cameraType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package com.example.godeye.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.example.godeye.R
|
|
||||||
import com.example.godeye.models.ConnectionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Компонент для отображения статуса подключения к серверу
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ConnectionStatusCard(
|
|
||||||
connectionState: ConnectionState,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val (icon, color, statusText) = getConnectionStateInfo(connectionState)
|
|
||||||
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = color.copy(alpha = 0.1f)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
// Иконка с анимацией для состояний загрузки
|
|
||||||
ConnectionIcon(
|
|
||||||
icon = icon,
|
|
||||||
color = color,
|
|
||||||
isAnimated = connectionState == ConnectionState.CONNECTING ||
|
|
||||||
connectionState == ConnectionState.RECONNECTING
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
|
|
||||||
// Текст статуса
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.connection_status_label),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = statusText,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Анимированная иконка подключения
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun ConnectionIcon(
|
|
||||||
icon: ImageVector,
|
|
||||||
color: androidx.compose.ui.graphics.Color,
|
|
||||||
isAnimated: Boolean,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
if (isAnimated) {
|
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "connection_animation")
|
|
||||||
val rotation by infiniteTransition.animateFloat(
|
|
||||||
initialValue = 0f,
|
|
||||||
targetValue = 360f,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = tween(1000, easing = LinearEasing),
|
|
||||||
repeatMode = RepeatMode.Restart
|
|
||||||
),
|
|
||||||
label = "rotation"
|
|
||||||
)
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.rotate(rotation),
|
|
||||||
tint = color
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = modifier.size(24.dp),
|
|
||||||
tint = color
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить информацию о состоянии подключения
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun getConnectionStateInfo(
|
|
||||||
connectionState: ConnectionState
|
|
||||||
): Triple<ImageVector, androidx.compose.ui.graphics.Color, String> {
|
|
||||||
return when (connectionState) {
|
|
||||||
ConnectionState.DISCONNECTED -> Triple(
|
|
||||||
Icons.Default.Close,
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
stringResource(R.string.status_disconnected)
|
|
||||||
)
|
|
||||||
ConnectionState.CONNECTING -> Triple(
|
|
||||||
Icons.Default.Refresh,
|
|
||||||
MaterialTheme.colorScheme.primary,
|
|
||||||
stringResource(R.string.status_connecting)
|
|
||||||
)
|
|
||||||
ConnectionState.CONNECTED -> Triple(
|
|
||||||
Icons.Default.CheckCircle,
|
|
||||||
MaterialTheme.colorScheme.primary,
|
|
||||||
stringResource(R.string.status_connected)
|
|
||||||
)
|
|
||||||
ConnectionState.ERROR -> Triple(
|
|
||||||
Icons.Default.Warning,
|
|
||||||
MaterialTheme.colorScheme.error,
|
|
||||||
stringResource(R.string.status_error)
|
|
||||||
)
|
|
||||||
ConnectionState.RECONNECTING -> Triple(
|
|
||||||
Icons.Default.Refresh,
|
|
||||||
MaterialTheme.colorScheme.secondary,
|
|
||||||
stringResource(R.string.status_reconnecting)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,895 @@
|
|||||||
|
package com.example.godeye.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.godeye.models.*
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ConnectionStatusIndicator(connectionState: ConnectionState) {
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "connection_indicator")
|
||||||
|
|
||||||
|
val animatedColor by infiniteTransition.animateColor(
|
||||||
|
initialValue = when (connectionState) {
|
||||||
|
ConnectionState.CONNECTED -> GodEyeColors.SuccessGreen
|
||||||
|
ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> GodEyeColors.WarningAmber
|
||||||
|
ConnectionState.ERROR -> GodEyeColors.RecordRed
|
||||||
|
ConnectionState.DISCONNECTED -> GodEyeColors.IvorySoft
|
||||||
|
},
|
||||||
|
targetValue = when (connectionState) {
|
||||||
|
ConnectionState.CONNECTED -> GodEyeColors.SuccessGreen.copy(alpha = 0.7f)
|
||||||
|
ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> GodEyeColors.WarningAmber.copy(alpha = 0.7f)
|
||||||
|
ConnectionState.ERROR -> GodEyeColors.RecordRed.copy(alpha = 0.7f)
|
||||||
|
ConnectionState.DISCONNECTED -> GodEyeColors.IvorySoft.copy(alpha = 0.7f)
|
||||||
|
},
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1500, easing = EaseInOut),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "indicator_color"
|
||||||
|
)
|
||||||
|
|
||||||
|
val scale by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 1f,
|
||||||
|
targetValue = if (connectionState in listOf(ConnectionState.CONNECTING, ConnectionState.RECONNECTING)) 1.2f else 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = EaseInOut),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "indicator_scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.scale(scale)
|
||||||
|
.background(
|
||||||
|
brush = Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
animatedColor,
|
||||||
|
animatedColor.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServerConfigurationPrompt(onSettingsClick: () -> Unit) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.1f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.WarningAmber,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Сервер не настроен",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Откройте настройки для выбора сервера",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onSettingsClick,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.NavyLight
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Открыть настройки")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ServerInfoDisplay(
|
||||||
|
serverUrl: String,
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
|
onUpdateUrl: (String) -> Unit,
|
||||||
|
connectionState: ConnectionState,
|
||||||
|
isLoading: Boolean
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = onUpdateUrl,
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
"Server URL",
|
||||||
|
color = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
"http://192.168.1.100:3001",
|
||||||
|
color = GodEyeColors.IvorySoft.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = connectionState != ConnectionState.CONNECTED && !isLoading,
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
focusedTextColor = GodEyeColors.IvoryPure,
|
||||||
|
unfocusedTextColor = GodEyeColors.IvorySoft
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = onSettingsClick) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Settings,
|
||||||
|
contentDescription = "Настройки",
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ConnectionControls(
|
||||||
|
connectionState: ConnectionState,
|
||||||
|
isLoading: Boolean,
|
||||||
|
serverUrl: String,
|
||||||
|
onConnect: () -> Unit,
|
||||||
|
onDisconnect: () -> Unit,
|
||||||
|
onSettings: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = connectionState,
|
||||||
|
transitionSpec = {
|
||||||
|
slideInHorizontally(
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
|
||||||
|
) togetherWith slideOutHorizontally(
|
||||||
|
animationSpec = tween(300)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = "connection_controls"
|
||||||
|
) { state ->
|
||||||
|
when (state) {
|
||||||
|
ConnectionState.CONNECTED -> {
|
||||||
|
Button(
|
||||||
|
onClick = onDisconnect,
|
||||||
|
enabled = !isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.PowerOff, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
"Отключиться от сервера",
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (serverUrl.isBlank()) onSettings() else onConnect()
|
||||||
|
},
|
||||||
|
enabled = !isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.NavyLight
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Подключение...",
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Link, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
if (serverUrl.isBlank()) "Настроить сервер" else "Подключиться к серверу",
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OperatorRequestCard(
|
||||||
|
cameraRequest: CameraRequest,
|
||||||
|
onAccept: () -> Unit,
|
||||||
|
onReject: () -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedFloatingPanel(
|
||||||
|
visible = true,
|
||||||
|
slideDirection = SlideDirection.FromBottom
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Videocam,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.WarningAmber,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Запрос на подключение к камере",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Оператор ${cameraRequest.operatorId}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackMedium.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
InfoRow("Тип камеры", cameraRequest.cameraType)
|
||||||
|
InfoRow("Session ID", cameraRequest.sessionId.take(12) + "...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onReject,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Отклонить", color = GodEyeColors.IvoryPure)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onAccept,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.SuccessGreen
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Разрешить", color = GodEyeColors.IvoryPure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActiveSessionsCard(
|
||||||
|
activeSessions: Map<String, SessionInfo>,
|
||||||
|
onEndSession: (String) -> Unit
|
||||||
|
) {
|
||||||
|
AnimatedFloatingPanel(
|
||||||
|
visible = true,
|
||||||
|
slideDirection = SlideDirection.FromBottom
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.VideoCall,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.RecordRed,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Активная трансляция",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Активных сессий: ${activeSessions.size}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSessions.forEach { (sessionId, sessionInfo) ->
|
||||||
|
SessionCard(
|
||||||
|
sessionId = sessionId,
|
||||||
|
sessionInfo = sessionInfo,
|
||||||
|
onEndSession = onEndSession
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SessionCard(
|
||||||
|
sessionId: String,
|
||||||
|
sessionInfo: SessionInfo,
|
||||||
|
onEndSession: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = "Оператор: ${sessionInfo.operatorId}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Камера: ${sessionInfo.cameraType}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Статус: ${sessionInfo.status}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onEndSession(sessionId) },
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.8f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Завершить", color = GodEyeColors.IvoryPure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SuccessStatusCard(
|
||||||
|
cameraRequest: CameraRequest?,
|
||||||
|
isStreaming: Boolean
|
||||||
|
) {
|
||||||
|
AnimatedFloatingPanel(
|
||||||
|
visible = true,
|
||||||
|
slideDirection = SlideDirection.FromBottom
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.SuccessGreen,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Успешно подключено!",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Android устройство зарегистрировано на сервере",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when {
|
||||||
|
cameraRequest != null -> "📷 Получен запрос на подключение к камере"
|
||||||
|
isStreaming -> "🔴 Трансляция активна"
|
||||||
|
else -> "Ожидание запросов от операторов..."
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(label: String, value: String) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сворачиваемая плитка для главного экрана
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CollapsibleTile(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
expanded: Boolean,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
statusColor: androidx.compose.ui.graphics.Color = GodEyeColors.IvorySoft,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.8f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
// Заголовок плитки (всегда видимый)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onToggle() }
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = statusColor,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||||
|
contentDescription = if (expanded) "Свернуть" else "Развернуть",
|
||||||
|
tint = GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Развернутое содержимое
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = expanded,
|
||||||
|
enter = expandVertically(
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
|
||||||
|
) + fadeIn(),
|
||||||
|
exit = shrinkVertically(
|
||||||
|
animationSpec = tween(300)
|
||||||
|
) + fadeOut()
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Плитка-кнопка для действий
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ActionTile(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
backgroundColor: androidx.compose.ui.graphics.Color = GodEyeColors.NavyMedium
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = enabled) { onClick() },
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (enabled) backgroundColor else GodEyeColors.BlackMedium.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = if (enabled) 6.dp else 2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(20.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (enabled) GodEyeColors.IvoryPure else GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = if (enabled) GodEyeColors.IvoryPure else GodEyeColors.IvorySoft.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (enabled) GodEyeColors.IvorySoft else GodEyeColors.IvorySoft.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Карточка активной сессии
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ActiveSessionCard(
|
||||||
|
sessionInfo: SessionInfo,
|
||||||
|
onEndSession: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.SuccessGreen.copy(alpha = 0.1f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
border = BorderStroke(1.dp, GodEyeColors.SuccessGreen.copy(alpha = 0.3f))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Оператор: ${sessionInfo.operatorId.take(8)}...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Камера: ${sessionInfo.cameraType}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Статус: ${sessionInfo.status}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.SuccessGreen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onEndSession,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Stop,
|
||||||
|
contentDescription = "Завершить сессию",
|
||||||
|
tint = GodEyeColors.RecordRed,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CameraPreviewCard(
|
||||||
|
isStreaming: Boolean,
|
||||||
|
activeSessions: Map<String, SessionInfo>,
|
||||||
|
onStartStreaming: () -> Unit,
|
||||||
|
onStopStreaming: () -> Unit,
|
||||||
|
onSwitchCamera: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var currentCamera by remember { mutableStateOf("back") }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.8f)
|
||||||
|
),
|
||||||
|
border = BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
if (isStreaming) GodEyeColors.SuccessGreen else GodEyeColors.NavyLight
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Фон предпросмотра
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
GodEyeColors.BlackSoft,
|
||||||
|
GodEyeColors.NavyDark
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isStreaming) {
|
||||||
|
// Анимированный индикатор стриминга
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "streaming")
|
||||||
|
val alpha by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0.3f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
),
|
||||||
|
label = "streaming_alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Videocam,
|
||||||
|
contentDescription = "Streaming",
|
||||||
|
tint = GodEyeColors.SuccessGreen.copy(alpha = alpha),
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "LIVE",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GodEyeColors.SuccessGreen.copy(alpha = alpha)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${activeSessions.size} активных сессий",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.VideocamOff,
|
||||||
|
contentDescription = "Not streaming",
|
||||||
|
tint = GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Камера готова",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Ожидание запроса от оператора",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Верхняя панель управления
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Индикатор качества
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackPure.copy(alpha = 0.7f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "1280x720",
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка переключения камеры
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
currentCamera = if (currentCamera == "back") "front" else "back"
|
||||||
|
onSwitchCamera(currentCamera)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
GodEyeColors.BlackPure.copy(alpha = 0.7f),
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.FlipCameraAndroid,
|
||||||
|
contentDescription = "Switch camera",
|
||||||
|
tint = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нижняя панель управления
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Кнопка управления стримингом
|
||||||
|
if (isStreaming) {
|
||||||
|
Button(
|
||||||
|
onClick = onStopStreaming,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.RecordRed
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Stop,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Остановить")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = onStartStreaming,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.SuccessGreen
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.PlayArrow,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Запустить")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
package com.example.godeye.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Person
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.example.godeye.R
|
|
||||||
import com.example.godeye.models.CameraSession
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Компонент для отображения списка активных сессий
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SessionsList(
|
|
||||||
sessions: List<CameraSession>,
|
|
||||||
onEndSession: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
if (sessions.isEmpty()) {
|
|
||||||
// Пустое состояние
|
|
||||||
Box(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_active_sessions),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
contentPadding = PaddingValues(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
items(sessions, key = { it.sessionId }) { session ->
|
|
||||||
SessionItem(
|
|
||||||
session = session,
|
|
||||||
onEndSession = { onEndSession(session.sessionId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Элемент списка сессий
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SessionItem(
|
|
||||||
session: CameraSession,
|
|
||||||
onEndSession: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
// Заголовок с оператором
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Person,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.session_operator_label)} ${session.operatorId}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Статус WebRTC
|
|
||||||
WebRTCStatusChip(isConnected = session.webRTCConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Информация о камере
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Person, // Заменено с Camera на Person
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.session_camera_label)} ${getCameraTypeName(session.cameraType)}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Длительность сессии
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
SessionDuration(startTime = session.startTime)
|
|
||||||
|
|
||||||
// Кнопка завершения сессии
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = onEndSession,
|
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Close,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(stringResource(R.string.end_session_button))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Компонент для отображения статуса WebRTC соединения
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun WebRTCStatusChip(
|
|
||||||
isConnected: Boolean,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val backgroundColor = if (isConnected) {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.errorContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentColor = if (isConnected) {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusText = if (isConnected) {
|
|
||||||
stringResource(R.string.webrtc_connected)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.webrtc_disconnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
|
||||||
modifier = modifier,
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
color = backgroundColor
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.session_webrtc_status, statusText),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = contentColor,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Компонент для отображения длительности сессии
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun SessionDuration(
|
|
||||||
startTime: Long,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var currentTime by remember { mutableStateOf(System.currentTimeMillis()) }
|
|
||||||
|
|
||||||
// Обновляем время каждую секунду
|
|
||||||
LaunchedEffect(startTime) {
|
|
||||||
while (true) {
|
|
||||||
currentTime = System.currentTimeMillis()
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val duration = currentTime - startTime
|
|
||||||
val hours = (duration / (1000 * 60 * 60)) % 24
|
|
||||||
val minutes = (duration / (1000 * 60)) % 60
|
|
||||||
val seconds = (duration / 1000) % 60
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(R.string.session_duration_label)} ${String.format("%02d:%02d:%02d", hours, minutes, seconds)}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить локализованное название типа камеры
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun getCameraTypeName(cameraType: String): String {
|
|
||||||
return when (cameraType) {
|
|
||||||
Constants.CameraTypes.BACK -> stringResource(R.string.camera_type_back)
|
|
||||||
Constants.CameraTypes.FRONT -> stringResource(R.string.camera_type_front)
|
|
||||||
Constants.CameraTypes.WIDE -> stringResource(R.string.camera_type_wide)
|
|
||||||
Constants.CameraTypes.TELEPHOTO -> stringResource(R.string.camera_type_telephoto)
|
|
||||||
else -> cameraType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,512 @@
|
|||||||
|
package com.example.godeye.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
import com.example.godeye.utils.getPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Экран настроек GodEye с расширенными параметрами
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onServerConfigSaved: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val prefs = context.getPreferences()
|
||||||
|
|
||||||
|
// Состояния настроек
|
||||||
|
var serverUrl by remember {
|
||||||
|
mutableStateOf(prefs.getString("server_url", "http://192.168.219.108:3001") ?: "")
|
||||||
|
}
|
||||||
|
var deviceName by remember {
|
||||||
|
mutableStateOf(prefs.getString("device_name", "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}") ?: "")
|
||||||
|
}
|
||||||
|
var autoConnect by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("auto_connect", false))
|
||||||
|
}
|
||||||
|
var autoAcceptRequests by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("auto_accept_requests", true))
|
||||||
|
}
|
||||||
|
var enableNotifications by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("enable_notifications", true))
|
||||||
|
}
|
||||||
|
var keepScreenOn by remember {
|
||||||
|
mutableStateOf(prefs.getBoolean("keep_screen_on", false))
|
||||||
|
}
|
||||||
|
var preferredCamera by remember {
|
||||||
|
mutableStateOf(prefs.getString("preferred_camera", "back") ?: "back")
|
||||||
|
}
|
||||||
|
var streamQuality by remember {
|
||||||
|
mutableStateOf(prefs.getString("stream_quality", "720p") ?: "720p")
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.systemBarsPadding()
|
||||||
|
) {
|
||||||
|
// Шапка экрана
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Настройки GodEye",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackPressed) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = "Назад",
|
||||||
|
tint = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
// Сохраняем все настройки
|
||||||
|
prefs.edit {
|
||||||
|
putString("server_url", serverUrl)
|
||||||
|
putString("device_name", deviceName)
|
||||||
|
putBoolean("auto_connect", autoConnect)
|
||||||
|
putBoolean("auto_accept_requests", autoAcceptRequests)
|
||||||
|
putBoolean("enable_notifications", enableNotifications)
|
||||||
|
putBoolean("keep_screen_on", keepScreenOn)
|
||||||
|
putString("preferred_camera", preferredCamera)
|
||||||
|
putString("stream_quality", streamQuality)
|
||||||
|
}
|
||||||
|
onServerConfigSaved(serverUrl)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Сохранить",
|
||||||
|
color = GodEyeColors.SuccessGreen,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = GodEyeColors.BlackSoft.copy(alpha = 0.9f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Секция "Сервер"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Подключение к серверу") {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = serverUrl,
|
||||||
|
onValueChange = { serverUrl = it },
|
||||||
|
label = { Text("URL сервера") },
|
||||||
|
placeholder = { Text("http://192.168.1.100:3001") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
focusedTextColor = GodEyeColors.IvoryPure,
|
||||||
|
unfocusedTextColor = GodEyeColors.IvorySoft,
|
||||||
|
focusedLabelColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedLabelColor = GodEyeColors.IvorySoft
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Language,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Автоматическое подключение",
|
||||||
|
subtitle = "Подключаться к серверу при запуске приложения",
|
||||||
|
checked = autoConnect,
|
||||||
|
onCheckedChange = { autoConnect = it },
|
||||||
|
icon = Icons.Default.AutoAwesome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "Устройство"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Устройство") {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = deviceName,
|
||||||
|
onValueChange = { deviceName = it },
|
||||||
|
label = { Text("Имя устройства") },
|
||||||
|
placeholder = { Text("Android Device") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedBorderColor = GodEyeColors.IvorySoft.copy(alpha = 0.5f),
|
||||||
|
focusedTextColor = GodEyeColors.IvoryPure,
|
||||||
|
unfocusedTextColor = GodEyeColors.IvorySoft,
|
||||||
|
focusedLabelColor = GodEyeColors.NavyLight,
|
||||||
|
unfocusedLabelColor = GodEyeColors.IvorySoft
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Smartphone,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Это имя будет отображаться операторам при подключении",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.padding(start = 48.dp, top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "Автоматизация"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Автоматизация") {
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Автоматическое принятие запросов",
|
||||||
|
subtitle = "Автоматически принимать запросы от операторов",
|
||||||
|
checked = autoAcceptRequests,
|
||||||
|
onCheckedChange = { autoAcceptRequests = it },
|
||||||
|
icon = Icons.Default.AutoAwesome
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Уведомления",
|
||||||
|
subtitle = "Показывать уведомления о входящих запросах",
|
||||||
|
checked = enableNotifications,
|
||||||
|
onCheckedChange = { enableNotifications = it },
|
||||||
|
icon = Icons.Default.Notifications
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingsSwitchCard(
|
||||||
|
title = "Не выключать экран",
|
||||||
|
subtitle = "Экран остается включенным во время сессии",
|
||||||
|
checked = keepScreenOn,
|
||||||
|
onCheckedChange = { keepScreenOn = it },
|
||||||
|
icon = Icons.Default.ScreenLockPortrait
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "Камера"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "Камера") {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.CameraAlt,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Предпочитаемая камера",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Камера по умолчанию для стриминга",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
onClick = { preferredCamera = "back" },
|
||||||
|
label = { Text("Основная") },
|
||||||
|
selected = preferredCamera == "back",
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = GodEyeColors.NavyLight,
|
||||||
|
selectedLabelColor = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
onClick = { preferredCamera = "front" },
|
||||||
|
label = { Text("Фронтальная") },
|
||||||
|
selected = preferredCamera == "front",
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = GodEyeColors.NavyLight,
|
||||||
|
selectedLabelColor = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.HighQuality,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Качество видео",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Разрешение видео потока",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
onClick = { streamQuality = "480p" },
|
||||||
|
label = { Text("480p") },
|
||||||
|
selected = streamQuality == "480p",
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = GodEyeColors.NavyLight,
|
||||||
|
selectedLabelColor = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
onClick = { streamQuality = "720p" },
|
||||||
|
label = { Text("720p") },
|
||||||
|
selected = streamQuality == "720p",
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = GodEyeColors.NavyLight,
|
||||||
|
selectedLabelColor = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
onClick = { streamQuality = "1080p" },
|
||||||
|
label = { Text("1080p") },
|
||||||
|
selected = streamQuality == "1080p",
|
||||||
|
colors = FilterChipDefaults.filterChipColors(
|
||||||
|
selectedContainerColor = GodEyeColors.NavyLight,
|
||||||
|
selectedLabelColor = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Секция "О приложении"
|
||||||
|
item {
|
||||||
|
SettingsSection(title = "О приложении") {
|
||||||
|
InfoCard(
|
||||||
|
title = "GodEye Android Client",
|
||||||
|
subtitle = "Версия 1.0.0 (Build 1)",
|
||||||
|
icon = Icons.Default.Info
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
InfoCard(
|
||||||
|
title = "Device ID",
|
||||||
|
subtitle = context.getPreferences().getString("device_id", "Неизвестно") ?: "Неизвестно",
|
||||||
|
icon = Icons.Default.Fingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsSection(
|
||||||
|
title: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = GodEyeColors.IvoryPure,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsSwitchCard(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (checked) GodEyeColors.SuccessGreen else GodEyeColors.IvorySoft,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = GodEyeColors.IvoryPure,
|
||||||
|
checkedTrackColor = GodEyeColors.SuccessGreen,
|
||||||
|
uncheckedThumbColor = GodEyeColors.IvorySoft,
|
||||||
|
uncheckedTrackColor = GodEyeColors.NavyDark
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoCard(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.NavyDark.copy(alpha = 0.3f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = GodEyeColors.NavyLight,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
package com.example.godeye.ui.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.example.godeye.models.CameraRequest
|
||||||
|
import com.example.godeye.ui.theme.GodEyeColors
|
||||||
|
import com.example.godeye.ui.theme.GodEyeTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CameraRequestDialog - диалог запроса доступа к камере согласно ТЗ
|
||||||
|
* Отображает информацию об операторе и запрашиваемой камере
|
||||||
|
*/
|
||||||
|
class CameraRequestDialog : DialogFragment() {
|
||||||
|
|
||||||
|
private var cameraRequest: CameraRequest? = null
|
||||||
|
private var onAccept: (() -> Unit)? = null
|
||||||
|
private var onReject: (() -> Unit)? = null
|
||||||
|
private var autoAccept: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialog = Dialog(requireContext())
|
||||||
|
dialog.setContentView(ComposeView(requireContext()).apply {
|
||||||
|
setContent {
|
||||||
|
GodEyeTheme {
|
||||||
|
CameraRequestDialogContent(
|
||||||
|
cameraRequest = cameraRequest,
|
||||||
|
autoAccept = autoAccept,
|
||||||
|
onAccept = {
|
||||||
|
onAccept?.invoke()
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
onReject = {
|
||||||
|
onReject?.invoke()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
dialog.setCancelable(false) // Пользователь должен явно принять решение
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(
|
||||||
|
request: CameraRequest,
|
||||||
|
autoAccept: Boolean = false,
|
||||||
|
onAccept: () -> Unit,
|
||||||
|
onReject: () -> Unit
|
||||||
|
): CameraRequestDialog {
|
||||||
|
return CameraRequestDialog().apply {
|
||||||
|
this.cameraRequest = request
|
||||||
|
this.autoAccept = autoAccept
|
||||||
|
this.onAccept = onAccept
|
||||||
|
this.onReject = onReject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CameraRequestDialogContent(
|
||||||
|
cameraRequest: CameraRequest?,
|
||||||
|
autoAccept: Boolean,
|
||||||
|
onAccept: () -> Unit,
|
||||||
|
onReject: () -> Unit
|
||||||
|
) {
|
||||||
|
if (cameraRequest == null) return
|
||||||
|
|
||||||
|
// Автоматическое принятие если включено
|
||||||
|
if (autoAccept) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(500) // Небольшая задержка для показа диалога
|
||||||
|
onAccept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = { /* Нельзя закрыть без выбора */ }) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(24.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackSoft
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
GodEyeColors.NavyDark.copy(alpha = 0.3f),
|
||||||
|
GodEyeColors.BlackSoft
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Иконка оператора
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
GodEyeColors.NavyLight.copy(alpha = 0.3f),
|
||||||
|
GodEyeColors.NavyDark.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RoundedCornerShape(50)
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Person,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заголовок
|
||||||
|
Text(
|
||||||
|
text = "Запрос доступа к камере",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
|
||||||
|
// Информация о запросе согласно ТЗ
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = GodEyeColors.BlackMedium.copy(alpha = 0.5f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
InfoRow(
|
||||||
|
label = "Оператор",
|
||||||
|
value = cameraRequest.operatorId,
|
||||||
|
icon = Icons.Default.Person
|
||||||
|
)
|
||||||
|
|
||||||
|
InfoRow(
|
||||||
|
label = "Камера",
|
||||||
|
value = getCameraDisplayName(cameraRequest.cameraType),
|
||||||
|
icon = Icons.Default.Videocam
|
||||||
|
)
|
||||||
|
|
||||||
|
InfoRow(
|
||||||
|
label = "Session ID",
|
||||||
|
value = cameraRequest.sessionId.take(12) + "...",
|
||||||
|
icon = Icons.Default.Key
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Описание запроса
|
||||||
|
Text(
|
||||||
|
text = "Оператор ${cameraRequest.operatorId} запрашивает доступ к камере ${getCameraDisplayName(cameraRequest.cameraType)}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = GodEyeColors.IvorySoft,
|
||||||
|
lineHeight = MaterialTheme.typography.bodyMedium.lineHeight * 1.4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Индикатор автоматического принятия
|
||||||
|
if (autoAccept) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
color = GodEyeColors.SuccessGreen
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Автоматическое принятие...",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.SuccessGreen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопки действий согласно ТЗ
|
||||||
|
if (!autoAccept) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Кнопка "Отклонить"
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onReject,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = GodEyeColors.RecordRed
|
||||||
|
),
|
||||||
|
border = ButtonDefaults.outlinedButtonBorder.copy(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
GodEyeColors.RecordRed,
|
||||||
|
GodEyeColors.RecordRed.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text("Отклонить")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка "Разрешить"
|
||||||
|
Button(
|
||||||
|
onClick = onAccept,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = GodEyeColors.SuccessGreen
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
"Разрешить",
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Опция "Запомнить для этого оператора" согласно ТЗ
|
||||||
|
if (!autoAccept) {
|
||||||
|
var rememberChoice by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = rememberChoice,
|
||||||
|
onCheckedChange = { rememberChoice = it },
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = GodEyeColors.NavyLight,
|
||||||
|
uncheckedColor = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Запомнить для этого оператора",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InfoRow(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = GodEyeColors.IvorySoft
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = GodEyeColors.IvoryPure
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение человекочитаемого названия камеры согласно ТЗ
|
||||||
|
*/
|
||||||
|
private fun getCameraDisplayName(cameraType: String): String {
|
||||||
|
return when (cameraType) {
|
||||||
|
"back" -> "Основная камера"
|
||||||
|
"front" -> "Фронтальная камера"
|
||||||
|
"ultra_wide" -> "Широкоугольная камера"
|
||||||
|
"telephoto" -> "Телеобъектив"
|
||||||
|
else -> "Камера ($cameraType)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
package com.example.godeye.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Settings
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.example.godeye.R
|
|
||||||
import com.example.godeye.models.ConnectionState
|
|
||||||
import com.example.godeye.models.MainScreenState
|
|
||||||
import com.example.godeye.ui.components.CameraRequestDialog
|
|
||||||
import com.example.godeye.ui.components.ConnectionStatusCard
|
|
||||||
import com.example.godeye.ui.components.SessionsList
|
|
||||||
import com.example.godeye.ui.viewmodels.MainViewModel
|
|
||||||
import com.example.godeye.ui.viewmodels.UiEvent
|
|
||||||
import com.example.godeye.utils.collectAsEffect
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Главный экран приложения
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MainScreen(
|
|
||||||
viewModel: MainViewModel = viewModel(),
|
|
||||||
onRequestPermissions: () -> Unit,
|
|
||||||
onShowError: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
|
||||||
var serverUrl by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
// Синхронизируем локальное состояние с ViewModel
|
|
||||||
LaunchedEffect(uiState.serverUrl) {
|
|
||||||
serverUrl = uiState.serverUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка UI событий
|
|
||||||
LaunchedEffect(viewModel) {
|
|
||||||
viewModel.events.collect { event ->
|
|
||||||
when (event) {
|
|
||||||
is UiEvent.RequestPermissions -> onRequestPermissions()
|
|
||||||
is UiEvent.ShowError -> {
|
|
||||||
// Получаем текст ошибки внутри LaunchedEffect
|
|
||||||
val errorMessage = when (event.error) {
|
|
||||||
is com.example.godeye.models.AppError.NetworkError -> "Ошибка сети"
|
|
||||||
is com.example.godeye.models.AppError.CameraPermissionDenied -> "Нет разрешения на камеру"
|
|
||||||
is com.example.godeye.models.AppError.AudioPermissionDenied -> "Нет разрешения на микрофон"
|
|
||||||
is com.example.godeye.models.AppError.CameraNotAvailable -> "Камера недоступна"
|
|
||||||
is com.example.godeye.models.AppError.WebRTCConnectionFailed -> "Ошибка WebRTC соединения"
|
|
||||||
is com.example.godeye.models.AppError.SocketError -> "Ошибка WebSocket: ${event.error.message}"
|
|
||||||
is com.example.godeye.models.AppError.CameraError -> "Ошибка камеры: ${event.error.message}"
|
|
||||||
is com.example.godeye.models.AppError.UnknownError -> "Неизвестная ошибка"
|
|
||||||
}
|
|
||||||
onShowError(errorMessage)
|
|
||||||
}
|
|
||||||
is UiEvent.ShowMessage -> onShowError(event.message)
|
|
||||||
is UiEvent.ShowCameraRequestDialog -> {
|
|
||||||
// Диалог будет показан через состояние showCameraRequest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.app_name),
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = { /* TODO: Настройки */ }) {
|
|
||||||
Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.settings))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
MainContent(
|
|
||||||
uiState = uiState,
|
|
||||||
serverUrl = serverUrl,
|
|
||||||
onServerUrlChange = { serverUrl = it },
|
|
||||||
onConnect = {
|
|
||||||
viewModel.updateServerUrl(serverUrl)
|
|
||||||
viewModel.connect()
|
|
||||||
},
|
|
||||||
onDisconnect = viewModel::disconnect,
|
|
||||||
onEndSession = viewModel::endSession,
|
|
||||||
modifier = Modifier.padding(paddingValues)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Диалог запроса камеры
|
|
||||||
uiState.showCameraRequest?.let { request ->
|
|
||||||
CameraRequestDialog(
|
|
||||||
request = request,
|
|
||||||
onAccept = { viewModel.respondToCameraRequest(request.sessionId, true) },
|
|
||||||
onDeny = { viewModel.respondToCameraRequest(request.sessionId, false) },
|
|
||||||
onDismiss = { viewModel.respondToCameraRequest(request.sessionId, false) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Индикатор загрузки
|
|
||||||
if (uiState.isLoading) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(stringResource(R.string.loading))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Основное содержимое экрана
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun MainContent(
|
|
||||||
uiState: MainScreenState,
|
|
||||||
serverUrl: String,
|
|
||||||
onServerUrlChange: (String) -> Unit,
|
|
||||||
onConnect: () -> Unit,
|
|
||||||
onDisconnect: () -> Unit,
|
|
||||||
onEndSession: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// Информация об устройстве
|
|
||||||
DeviceInfoCard(deviceId = uiState.deviceId)
|
|
||||||
|
|
||||||
// Статус подключения
|
|
||||||
ConnectionStatusCard(connectionState = uiState.connectionState)
|
|
||||||
|
|
||||||
// Настройки подключения
|
|
||||||
ConnectionSettingsCard(
|
|
||||||
serverUrl = serverUrl,
|
|
||||||
onServerUrlChange = onServerUrlChange,
|
|
||||||
connectionState = uiState.connectionState,
|
|
||||||
onConnect = onConnect,
|
|
||||||
onDisconnect = onDisconnect
|
|
||||||
)
|
|
||||||
|
|
||||||
// Список активных сессий
|
|
||||||
ActiveSessionsCard(
|
|
||||||
sessions = uiState.activeSessions,
|
|
||||||
onEndSession = onEndSession
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Карточка с информацией об устройстве
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun DeviceInfoCard(
|
|
||||||
deviceId: String,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.device_id_label),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = deviceId.ifEmpty { "..." },
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Карточка настроек подключения
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun ConnectionSettingsCard(
|
|
||||||
serverUrl: String,
|
|
||||||
onServerUrlChange: (String) -> Unit,
|
|
||||||
connectionState: ConnectionState,
|
|
||||||
onConnect: () -> Unit,
|
|
||||||
onDisconnect: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
// Поле ввода URL сервера
|
|
||||||
OutlinedTextField(
|
|
||||||
value = serverUrl,
|
|
||||||
onValueChange = onServerUrlChange,
|
|
||||||
label = { Text(stringResource(R.string.server_url_label)) },
|
|
||||||
placeholder = { Text(stringResource(R.string.server_url_hint)) },
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
|
||||||
enabled = connectionState == ConnectionState.DISCONNECTED,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Кнопка подключения/отключения
|
|
||||||
val isConnected = connectionState == ConnectionState.CONNECTED
|
|
||||||
val isLoading = connectionState == ConnectionState.CONNECTING ||
|
|
||||||
connectionState == ConnectionState.RECONNECTING
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = if (isConnected) onDisconnect else onConnect,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
enabled = !isLoading && serverUrl.isNotBlank()
|
|
||||||
) {
|
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
if (isConnected) stringResource(R.string.disconnect_button)
|
|
||||||
else stringResource(R.string.connect_button)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Карточка активных сессий
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun ActiveSessionsCard(
|
|
||||||
sessions: List<com.example.godeye.models.CameraSession>,
|
|
||||||
onEndSession: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.active_sessions_label),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
|
|
||||||
if (sessions.isNotEmpty()) {
|
|
||||||
Badge {
|
|
||||||
Text("${sessions.size}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
SessionsList(
|
|
||||||
sessions = sessions,
|
|
||||||
onEndSession = onEndSession
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.example.godeye.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
|
||||||
val Pink40 = Color(0xFF7D5260)
|
|
||||||
112
app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
Normal file
112
app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package com.example.godeye.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Цветовая палитра GodEye согласно ТЗ
|
||||||
|
*/
|
||||||
|
object GodEyeColors {
|
||||||
|
// Основные цвета приложения
|
||||||
|
val BlackPure = Color(0xFF000000)
|
||||||
|
val BlackSoft = Color(0xFF1A1A1A)
|
||||||
|
val BlackMedium = Color(0xFF2D2D2D)
|
||||||
|
|
||||||
|
val IvoryPure = Color(0xFFFFFFF0)
|
||||||
|
val IvorySoft = Color(0xFFF5F5DC)
|
||||||
|
val IvoryMedium = Color(0xFFE6E6D4)
|
||||||
|
|
||||||
|
val NavyDark = Color(0xFF0F1419)
|
||||||
|
val NavyMedium = Color(0xFF1E2328)
|
||||||
|
val NavyLight = Color(0xFF2D3748)
|
||||||
|
|
||||||
|
// Функциональные цвета
|
||||||
|
val RecordRed = Color(0xFFFF3B30)
|
||||||
|
val WarningAmber = Color(0xFFFF9500)
|
||||||
|
val SuccessGreen = Color(0xFF30D158)
|
||||||
|
val InfoBlue = Color(0xFF007AFF)
|
||||||
|
|
||||||
|
// Градиенты
|
||||||
|
val PrimaryGradientStart = NavyDark
|
||||||
|
val PrimaryGradientEnd = BlackSoft
|
||||||
|
|
||||||
|
val AccentGradientStart = NavyLight
|
||||||
|
val AccentGradientEnd = NavyMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = GodEyeColors.NavyLight,
|
||||||
|
onPrimary = GodEyeColors.IvoryPure,
|
||||||
|
primaryContainer = GodEyeColors.NavyMedium,
|
||||||
|
onPrimaryContainer = GodEyeColors.IvorySoft,
|
||||||
|
|
||||||
|
secondary = GodEyeColors.IvoryMedium,
|
||||||
|
onSecondary = GodEyeColors.BlackPure,
|
||||||
|
secondaryContainer = GodEyeColors.BlackMedium,
|
||||||
|
onSecondaryContainer = GodEyeColors.IvoryPure,
|
||||||
|
|
||||||
|
tertiary = GodEyeColors.WarningAmber,
|
||||||
|
onTertiary = GodEyeColors.BlackPure,
|
||||||
|
|
||||||
|
error = GodEyeColors.RecordRed,
|
||||||
|
onError = GodEyeColors.IvoryPure,
|
||||||
|
|
||||||
|
background = GodEyeColors.BlackPure,
|
||||||
|
onBackground = GodEyeColors.IvoryPure,
|
||||||
|
|
||||||
|
surface = GodEyeColors.BlackSoft,
|
||||||
|
onSurface = GodEyeColors.IvoryPure,
|
||||||
|
surfaceVariant = GodEyeColors.BlackMedium,
|
||||||
|
onSurfaceVariant = GodEyeColors.IvorySoft,
|
||||||
|
|
||||||
|
outline = GodEyeColors.NavyMedium,
|
||||||
|
outlineVariant = GodEyeColors.NavyLight
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = GodEyeColors.NavyMedium,
|
||||||
|
onPrimary = GodEyeColors.IvoryPure,
|
||||||
|
primaryContainer = GodEyeColors.NavyLight,
|
||||||
|
onPrimaryContainer = GodEyeColors.BlackPure,
|
||||||
|
|
||||||
|
secondary = GodEyeColors.BlackMedium,
|
||||||
|
onSecondary = GodEyeColors.IvoryPure,
|
||||||
|
secondaryContainer = GodEyeColors.IvoryMedium,
|
||||||
|
onSecondaryContainer = GodEyeColors.BlackPure,
|
||||||
|
|
||||||
|
tertiary = GodEyeColors.WarningAmber,
|
||||||
|
onTertiary = GodEyeColors.IvoryPure,
|
||||||
|
|
||||||
|
error = GodEyeColors.RecordRed,
|
||||||
|
onError = GodEyeColors.IvoryPure,
|
||||||
|
|
||||||
|
background = GodEyeColors.IvoryPure,
|
||||||
|
onBackground = GodEyeColors.BlackPure,
|
||||||
|
|
||||||
|
surface = GodEyeColors.IvorySoft,
|
||||||
|
onSurface = GodEyeColors.BlackPure,
|
||||||
|
surfaceVariant = GodEyeColors.IvoryMedium,
|
||||||
|
onSurfaceVariant = GodEyeColors.BlackMedium,
|
||||||
|
|
||||||
|
outline = GodEyeColors.NavyLight,
|
||||||
|
outlineVariant = GodEyeColors.NavyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GodEyeTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography(),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.example.godeye.ui.theme
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
|
||||||
primary = Purple80,
|
|
||||||
secondary = PurpleGrey80,
|
|
||||||
tertiary = Pink80
|
|
||||||
)
|
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
|
||||||
primary = Purple40,
|
|
||||||
secondary = PurpleGrey40,
|
|
||||||
tertiary = Pink40
|
|
||||||
|
|
||||||
/* Other default colors to override
|
|
||||||
background = Color(0xFFFFFBFE),
|
|
||||||
surface = Color(0xFFFFFBFE),
|
|
||||||
onPrimary = Color.White,
|
|
||||||
onSecondary = Color.White,
|
|
||||||
onTertiary = Color.White,
|
|
||||||
onBackground = Color(0xFF1C1B1F),
|
|
||||||
onSurface = Color(0xFF1C1B1F),
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GodEyeTheme(
|
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val colorScheme = when {
|
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
|
||||||
else -> LightColorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
|
||||||
colorScheme = colorScheme,
|
|
||||||
typography = Typography,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.example.godeye.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
|
||||||
val Typography = Typography(
|
|
||||||
bodyLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
/* Other default text styles to override
|
|
||||||
titleLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 22.sp,
|
|
||||||
lineHeight = 28.sp,
|
|
||||||
letterSpacing = 0.sp
|
|
||||||
),
|
|
||||||
labelSmall = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
lineHeight = 16.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
)
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
package com.example.godeye.ui.viewmodels
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.example.godeye.managers.PermissionManager
|
|
||||||
import com.example.godeye.models.*
|
|
||||||
import com.example.godeye.services.CameraService
|
|
||||||
import com.example.godeye.services.SocketService
|
|
||||||
import com.example.godeye.utils.Constants
|
|
||||||
import com.example.godeye.utils.Logger
|
|
||||||
import com.example.godeye.utils.getPreferences
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewModel для главного экрана приложения
|
|
||||||
*/
|
|
||||||
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
|
|
||||||
private val context = getApplication<Application>()
|
|
||||||
private val permissionManager = PermissionManager(context)
|
|
||||||
|
|
||||||
// Сервисы
|
|
||||||
private var socketService: SocketService? = null
|
|
||||||
private var cameraService: CameraService? = null
|
|
||||||
private var socketServiceBound = false
|
|
||||||
private var cameraServiceBound = false
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
private val _uiState = MutableStateFlow(MainScreenState())
|
|
||||||
val uiState: StateFlow<MainScreenState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
// События для UI
|
|
||||||
private val _events = MutableSharedFlow<UiEvent>()
|
|
||||||
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
loadSavedSettings()
|
|
||||||
// startServices() убран отсюда
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Загрузить сохраненные настройки
|
|
||||||
*/
|
|
||||||
private fun loadSavedSettings() {
|
|
||||||
val prefs = context.getPreferences()
|
|
||||||
val serverUrl = prefs.getString(Constants.PreferenceKeys.SERVER_URL, Constants.DEFAULT_SERVER_URL) ?: Constants.DEFAULT_SERVER_URL
|
|
||||||
val deviceId = prefs.getString(Constants.PreferenceKeys.DEVICE_ID, "") ?: ""
|
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(
|
|
||||||
serverUrl = serverUrl,
|
|
||||||
deviceId = deviceId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Запустить сервисы (вызывать из MainActivity после проверки разрешений)
|
|
||||||
*/
|
|
||||||
fun startServices() {
|
|
||||||
// Запуск SocketService
|
|
||||||
val socketIntent = Intent(context, SocketService::class.java)
|
|
||||||
context.startForegroundService(socketIntent)
|
|
||||||
context.bindService(socketIntent, socketConnection, Context.BIND_AUTO_CREATE)
|
|
||||||
|
|
||||||
// Запуск CameraService
|
|
||||||
val cameraIntent = Intent(context, CameraService::class.java)
|
|
||||||
context.startForegroundService(cameraIntent)
|
|
||||||
context.bindService(cameraIntent, cameraConnection, Context.BIND_AUTO_CREATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceConnection для SocketService
|
|
||||||
*/
|
|
||||||
private val socketConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
||||||
val binder = service as SocketService.LocalBinder
|
|
||||||
socketService = binder.getService()
|
|
||||||
socketServiceBound = true
|
|
||||||
|
|
||||||
Logger.d("SocketService connected")
|
|
||||||
observeSocketService()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
|
||||||
socketServiceBound = false
|
|
||||||
socketService = null
|
|
||||||
Logger.d("SocketService disconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceConnection для CameraService
|
|
||||||
*/
|
|
||||||
private val cameraConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
||||||
val binder = service as CameraService.LocalBinder
|
|
||||||
cameraService = binder.getService()
|
|
||||||
cameraServiceBound = true
|
|
||||||
|
|
||||||
Logger.d("CameraService connected")
|
|
||||||
observeCameraService()
|
|
||||||
setupCameraServiceCallbacks()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
|
||||||
cameraServiceBound = false
|
|
||||||
cameraService = null
|
|
||||||
Logger.d("CameraService disconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Наблюдать за состоянием SocketService
|
|
||||||
*/
|
|
||||||
private fun observeSocketService() {
|
|
||||||
val service = socketService ?: return
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.connectionState.collect { state ->
|
|
||||||
_uiState.value = _uiState.value.copy(connectionState = state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.deviceId.collect { deviceId ->
|
|
||||||
_uiState.value = _uiState.value.copy(deviceId = deviceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.error.collect { error ->
|
|
||||||
error?.let {
|
|
||||||
_uiState.value = _uiState.value.copy(error = it)
|
|
||||||
_events.emit(UiEvent.ShowError(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.cameraRequest.collect { request ->
|
|
||||||
request?.let {
|
|
||||||
_uiState.value = _uiState.value.copy(showCameraRequest = it)
|
|
||||||
_events.emit(UiEvent.ShowCameraRequestDialog(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.webrtcOffer.collect { offer ->
|
|
||||||
offer?.let {
|
|
||||||
handleWebRTCOffer(it)
|
|
||||||
service.clearWebRTCOffer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.webrtcIceCandidate.collect { candidate ->
|
|
||||||
candidate?.let {
|
|
||||||
handleWebRTCIceCandidate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.cameraSwitchRequest.collect { request ->
|
|
||||||
request?.let { (sessionId, cameraType) ->
|
|
||||||
handleCameraSwitch(sessionId, cameraType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.sessionDisconnect.collect { sessionId ->
|
|
||||||
sessionId?.let {
|
|
||||||
handleSessionDisconnect(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Наблюдать за состоянием CameraService
|
|
||||||
*/
|
|
||||||
private fun observeCameraService() {
|
|
||||||
val service = cameraService ?: return
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.getSessionManager().activeSessions.collect { sessions ->
|
|
||||||
_uiState.value = _uiState.value.copy(activeSessions = sessions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
service.error.collect { error ->
|
|
||||||
error?.let {
|
|
||||||
_uiState.value = _uiState.value.copy(error = it)
|
|
||||||
_events.emit(UiEvent.ShowError(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Настроить callbacks для CameraService
|
|
||||||
*/
|
|
||||||
private fun setupCameraServiceCallbacks() {
|
|
||||||
cameraService?.setWebRTCCallbacks(
|
|
||||||
onOfferCreated = { sessionId, offer ->
|
|
||||||
// WebRTC offer создан, но в нашем случае мы получаем offer от оператора
|
|
||||||
Logger.d("WebRTC offer created for session: $sessionId")
|
|
||||||
},
|
|
||||||
onIceCandidateCreated = { sessionId, candidate, sdpMid, sdpMLineIndex ->
|
|
||||||
socketService?.sendIceCandidate(sessionId, candidate, sdpMid, sdpMLineIndex)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Подключиться к серверу
|
|
||||||
*/
|
|
||||||
fun connect() {
|
|
||||||
if (!permissionManager.hasCriticalPermissions()) {
|
|
||||||
_events.tryEmit(UiEvent.RequestPermissions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverUrl = _uiState.value.serverUrl
|
|
||||||
if (serverUrl.isBlank()) {
|
|
||||||
_events.tryEmit(UiEvent.ShowError(AppError.SocketError("Введите URL сервера")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем URL сервера
|
|
||||||
context.getPreferences().edit()
|
|
||||||
.putString(Constants.PreferenceKeys.SERVER_URL, serverUrl)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
|
||||||
socketService?.connect(serverUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Отключиться от сервера
|
|
||||||
*/
|
|
||||||
fun disconnect() {
|
|
||||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
|
||||||
socketService?.disconnect()
|
|
||||||
cameraService?.endAllSessions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обновить URL сервера
|
|
||||||
*/
|
|
||||||
fun updateServerUrl(url: String) {
|
|
||||||
_uiState.value = _uiState.value.copy(serverUrl = url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ответить на запрос камеры
|
|
||||||
*/
|
|
||||||
fun respondToCameraRequest(sessionId: String, accepted: Boolean, reason: String? = null) {
|
|
||||||
socketService?.sendCameraResponse(sessionId, accepted, reason)
|
|
||||||
|
|
||||||
if (accepted) {
|
|
||||||
// Получаем информацию о запросе
|
|
||||||
val request = _uiState.value.showCameraRequest
|
|
||||||
if (request != null && request.sessionId == sessionId) {
|
|
||||||
// Начинаем камера сессию
|
|
||||||
cameraService?.startCameraSession(sessionId, request.operatorId, request.cameraType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Очищаем запрос из UI
|
|
||||||
_uiState.value = _uiState.value.copy(showCameraRequest = null)
|
|
||||||
socketService?.clearCameraRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Завершить сессию
|
|
||||||
*/
|
|
||||||
fun endSession(sessionId: String) {
|
|
||||||
cameraService?.endSession(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать WebRTC Offer
|
|
||||||
*/
|
|
||||||
private fun handleWebRTCOffer(offer: WebRTCMessage) {
|
|
||||||
val sessionId = offer.sessionId
|
|
||||||
val offerSdp = offer.sdp ?: return
|
|
||||||
|
|
||||||
Logger.d("Handling WebRTC offer for session: $sessionId")
|
|
||||||
// В нашем случае мы не обрабатываем offer, так как создаем его сами
|
|
||||||
// Но можно добавить логику для обработки offer от оператора
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать WebRTC ICE Candidate
|
|
||||||
*/
|
|
||||||
private fun handleWebRTCIceCandidate(candidate: WebRTCMessage) {
|
|
||||||
val sessionId = candidate.sessionId
|
|
||||||
val candidateSdp = candidate.candidate ?: return
|
|
||||||
val sdpMid = candidate.sdpMid ?: return
|
|
||||||
val sdpMLineIndex = candidate.sdpMLineIndex ?: return
|
|
||||||
|
|
||||||
Logger.d("Handling ICE candidate for session: $sessionId")
|
|
||||||
cameraService?.addIceCandidate(sessionId, candidateSdp, sdpMid, sdpMLineIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать переключение камеры
|
|
||||||
*/
|
|
||||||
private fun handleCameraSwitch(sessionId: String, newCameraType: String) {
|
|
||||||
Logger.d("Handling camera switch for session $sessionId to $newCameraType")
|
|
||||||
cameraService?.switchCamera(sessionId, newCameraType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обработать отключение сессии
|
|
||||||
*/
|
|
||||||
private fun handleSessionDisconnect(sessionId: String) {
|
|
||||||
Logger.d("Handling session disconnect: $sessionId")
|
|
||||||
cameraService?.endSession(sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очистить ошибку
|
|
||||||
*/
|
|
||||||
fun clearError() {
|
|
||||||
_uiState.value = _uiState.value.copy(error = null)
|
|
||||||
socketService?.clearError()
|
|
||||||
cameraService?.clearError()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Проверить разрешения
|
|
||||||
*/
|
|
||||||
fun checkPermissions(): Boolean {
|
|
||||||
return permissionManager.hasAllRequiredPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить отсутствующие разрешения
|
|
||||||
*/
|
|
||||||
fun getMissingPermissions(): List<String> {
|
|
||||||
return permissionManager.getMissingPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
|
|
||||||
// Отвязка сервисов
|
|
||||||
try {
|
|
||||||
if (socketServiceBound) {
|
|
||||||
context.unbindService(socketConnection)
|
|
||||||
socketServiceBound = false
|
|
||||||
}
|
|
||||||
if (cameraServiceBound) {
|
|
||||||
context.unbindService(cameraConnection)
|
|
||||||
cameraServiceBound = false
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.e("Error unbinding services", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.d("MainViewModel cleared")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* События UI для обработки в Activity/Compose
|
|
||||||
*/
|
|
||||||
sealed class UiEvent {
|
|
||||||
object RequestPermissions : UiEvent()
|
|
||||||
data class ShowError(val error: AppError) : UiEvent()
|
|
||||||
data class ShowCameraRequestDialog(val request: CameraRequest) : UiEvent()
|
|
||||||
data class ShowMessage(val message: String) : UiEvent()
|
|
||||||
}
|
|
||||||
@@ -1,49 +1,91 @@
|
|||||||
package com.example.godeye.utils
|
package com.example.godeye.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants - константы приложения согласно ТЗ
|
||||||
|
*/
|
||||||
object Constants {
|
object Constants {
|
||||||
// WebSocket события
|
|
||||||
object SocketEvents {
|
|
||||||
const val REGISTER_ANDROID = "register:android"
|
|
||||||
const val REGISTER_SUCCESS = "register:success"
|
|
||||||
const val REGISTER_ERROR = "register:error"
|
|
||||||
const val CAMERA_REQUEST = "camera:request"
|
|
||||||
const val CAMERA_RESPONSE = "camera:response"
|
|
||||||
const val CAMERA_DISCONNECT = "camera:disconnect"
|
|
||||||
const val CAMERA_SWITCH = "camera:switch"
|
|
||||||
const val WEBRTC_OFFER = "webrtc:offer"
|
|
||||||
const val WEBRTC_ANSWER = "webrtc:answer"
|
|
||||||
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Типы камер
|
// Настройки сервера согласно ТЗ
|
||||||
|
const val DEFAULT_SERVER_URL = "http://192.168.219.108:3001"
|
||||||
|
const val LOCALHOST_SERVER_URL = "http://localhost:3001"
|
||||||
|
const val LOCAL_NETWORK_SERVER_URL = "http://192.168.1.100:3001"
|
||||||
|
|
||||||
|
// Настройки Socket.IO
|
||||||
|
const val SOCKET_TIMEOUT = 10000L
|
||||||
|
const val SOCKET_RECONNECTION_ATTEMPTS = 5
|
||||||
|
const val SOCKET_RECONNECTION_DELAY = 1000L
|
||||||
|
|
||||||
|
// Настройки WebRTC согласно ТЗ
|
||||||
|
const val WEBRTC_VIDEO_WIDTH = 1920
|
||||||
|
const val WEBRTC_VIDEO_HEIGHT = 1080
|
||||||
|
const val WEBRTC_VIDEO_FPS = 30
|
||||||
|
|
||||||
|
// STUN серверы согласно ТЗ
|
||||||
|
val STUN_SERVERS = listOf(
|
||||||
|
"stun:stun.l.google.com:19302",
|
||||||
|
"stun:stun1.l.google.com:19302"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Типы камер согласно ТЗ
|
||||||
object CameraTypes {
|
object CameraTypes {
|
||||||
const val BACK = "back"
|
const val BACK = "back"
|
||||||
const val FRONT = "front"
|
const val FRONT = "front"
|
||||||
const val WIDE = "wide"
|
const val ULTRA_WIDE = "ultra_wide"
|
||||||
const val TELEPHOTO = "telephoto"
|
const val TELEPHOTO = "telephoto"
|
||||||
|
|
||||||
|
val ALL_TYPES = listOf(BACK, FRONT, ULTRA_WIDE, TELEPHOTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SharedPreferences ключи
|
// События Socket.IO согласно ТЗ
|
||||||
|
object SocketEvents {
|
||||||
|
// Исходящие события
|
||||||
|
const val REGISTER_ANDROID = "register:android"
|
||||||
|
const val CAMERA_RESPONSE = "camera:response"
|
||||||
|
const val WEBRTC_OFFER = "webrtc:offer"
|
||||||
|
const val WEBRTC_ANSWER = "webrtc:answer"
|
||||||
|
const val WEBRTC_ICE_CANDIDATE = "webrtc:ice-candidate"
|
||||||
|
|
||||||
|
// Входящие события
|
||||||
|
const val REGISTER_SUCCESS = "register:success"
|
||||||
|
const val CAMERA_REQUEST = "camera:request"
|
||||||
|
const val CAMERA_DISCONNECT = "camera:disconnect"
|
||||||
|
const val CAMERA_SWITCH = "camera:switch"
|
||||||
|
const val WEBRTC_OFFER_RECEIVED = "webrtc:offer"
|
||||||
|
const val WEBRTC_ANSWER_RECEIVED = "webrtc:answer"
|
||||||
|
const val WEBRTC_ICE_RECEIVED = "webrtc:ice-candidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SharedPreferences ключи согласно ТЗ
|
||||||
object PreferenceKeys {
|
object PreferenceKeys {
|
||||||
const val SERVER_URL = "server_url"
|
const val SERVER_URL = "server_url"
|
||||||
const val DEVICE_ID = "device_id"
|
const val DEVICE_ID = "device_id"
|
||||||
const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests"
|
const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests"
|
||||||
const val CAMERA_QUALITY = "camera_quality"
|
const val CAMERA_QUALITY = "camera_quality"
|
||||||
const val NOTIFICATION_ENABLED = "notification_enabled"
|
const val NOTIFICATION_ENABLED = "notification_enabled"
|
||||||
|
const val REMEMBER_OPERATORS = "remember_operators"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Настройки по умолчанию
|
// Настройки уведомлений
|
||||||
const val DEFAULT_SERVER_URL = "http://10.0.2.2:3001" // Специальный IP для Android эмулятора
|
|
||||||
const val SOCKET_CONNECTION_TIMEOUT = 10000L
|
|
||||||
const val WEBRTC_CONNECTION_TIMEOUT = 15000L
|
|
||||||
|
|
||||||
// Уведомления
|
|
||||||
const val NOTIFICATION_CHANNEL_ID = "godeye_service_channel"
|
const val NOTIFICATION_CHANNEL_ID = "godeye_service_channel"
|
||||||
const val FOREGROUND_SERVICE_ID = 1001
|
const val SERVICE_NOTIFICATION_ID = 1001
|
||||||
|
const val CAMERA_REQUEST_NOTIFICATION_ID = 1002
|
||||||
|
|
||||||
// WebRTC настройки
|
// Таймауты
|
||||||
val STUN_SERVERS = listOf(
|
const val CAMERA_OPEN_TIMEOUT = 2500L
|
||||||
"stun:stun.l.google.com:19302",
|
const val WEBRTC_CONNECTION_TIMEOUT = 10000L
|
||||||
"stun:stun1.l.google.com:19302"
|
const val SOCKET_CONNECTION_TIMEOUT = 5000L
|
||||||
)
|
|
||||||
|
// Версии API
|
||||||
|
const val MIN_SDK_VERSION = 24 // Android 7.0+ согласно ТЗ
|
||||||
|
const val TARGET_SDK_VERSION = 34
|
||||||
|
|
||||||
|
// Качество видео
|
||||||
|
object VideoQuality {
|
||||||
|
const val HD_WIDTH = 1280
|
||||||
|
const val HD_HEIGHT = 720
|
||||||
|
const val FULL_HD_WIDTH = 1920
|
||||||
|
const val FULL_HD_HEIGHT = 1080
|
||||||
|
const val FPS_30 = 30
|
||||||
|
const val FPS_60 = 60
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
146
app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
Normal file
146
app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package com.example.godeye.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import com.example.godeye.models.AppError
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorHandler - обработка ошибок согласно ТЗ
|
||||||
|
* Централизованная обработка всех типов ошибок приложения
|
||||||
|
*/
|
||||||
|
class ErrorHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка ошибок приложения согласно ТЗ
|
||||||
|
*/
|
||||||
|
fun handleError(error: AppError, context: Context, scope: CoroutineScope? = null, snackbarHost: SnackbarHostState? = null) {
|
||||||
|
Logger.error("APP_ERROR", "Handling application error: ${error::class.simpleName}", null)
|
||||||
|
|
||||||
|
when (error) {
|
||||||
|
is AppError.NetworkError -> {
|
||||||
|
showNetworkError(context, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
is AppError.CameraPermissionDenied -> {
|
||||||
|
showPermissionError(context, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
is AppError.CameraNotAvailable -> {
|
||||||
|
showCameraError(context, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
is AppError.WebRTCConnectionFailed -> {
|
||||||
|
showWebRTCError(context, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
is AppError.SocketError -> {
|
||||||
|
showSocketError(context, error.message, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
is AppError.UnknownError -> {
|
||||||
|
showUnknownError(context, error.throwable, scope, snackbarHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Специальная обработка исключений для предотвращения крашей
|
||||||
|
*/
|
||||||
|
fun handleUncaughtException(thread: Thread, throwable: Throwable) {
|
||||||
|
Logger.error("UNCAUGHT_EXCEPTION", "Uncaught exception in thread: ${thread.name}", throwable)
|
||||||
|
|
||||||
|
// Специальная обработка известных Compose ошибок
|
||||||
|
when {
|
||||||
|
throwable.message?.contains("ACTION_HOVER_EXIT event was not cleared") == true -> {
|
||||||
|
Logger.d("Ignoring Compose hover event bug")
|
||||||
|
// Игнорируем эту ошибку, так как это известный баг Compose
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throwable.message?.contains("Thread starting during runtime shutdown") == true -> {
|
||||||
|
Logger.d("Ignoring shutdown thread creation error")
|
||||||
|
// Игнорируем ошибки создания потоков при завершении
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Для остальных ошибок делаем стандартную обработку
|
||||||
|
Logger.error("CRITICAL_ERROR", "Critical error occurred", throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNetworkError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Ошибка сети. Проверьте подключение к интернету."
|
||||||
|
Logger.step("ERROR_NETWORK", message)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPermissionError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Необходимы разрешения для работы с камерой и микрофоном."
|
||||||
|
Logger.step("ERROR_PERMISSION", message)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCameraError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Камера недоступна. Проверьте, что другие приложения не используют камеру."
|
||||||
|
Logger.step("ERROR_CAMERA", message)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showWebRTCError(context: Context, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Ошибка WebRTC соединения. Попробуйте переподключиться."
|
||||||
|
Logger.step("ERROR_WEBRTC", message)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSocketError(context: Context, errorMessage: String, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Ошибка подключения к серверу: $errorMessage"
|
||||||
|
Logger.step("ERROR_SOCKET", message)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUnknownError(context: Context, throwable: Throwable, scope: CoroutineScope?, snackbarHost: SnackbarHostState?) {
|
||||||
|
val message = "Неизвестная ошибка: ${throwable.message ?: "Unknown"}"
|
||||||
|
Logger.error("ERROR_UNKNOWN", message, throwable)
|
||||||
|
|
||||||
|
if (scope != null && snackbarHost != null) {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHost.showSnackbar(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,139 +2,12 @@ package com.example.godeye.utils
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
|
||||||
import android.hardware.camera2.CameraManager
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
|
||||||
* Расширения для Context
|
|
||||||
*/
|
|
||||||
fun Context.getPreferences(): SharedPreferences {
|
fun Context.getPreferences(): SharedPreferences {
|
||||||
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
|
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.generateDeviceId(): String {
|
fun generateDeviceId(): String {
|
||||||
val prefs = getPreferences()
|
return "android_${UUID.randomUUID().toString().take(8)}"
|
||||||
var deviceId = prefs.getString(Constants.PreferenceKeys.DEVICE_ID, null)
|
|
||||||
if (deviceId == null) {
|
|
||||||
deviceId = "android_${UUID.randomUUID().toString().take(8)}"
|
|
||||||
prefs.edit().putString(Constants.PreferenceKeys.DEVICE_ID, deviceId).apply()
|
|
||||||
}
|
|
||||||
return deviceId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Расширения для CameraManager
|
|
||||||
*/
|
|
||||||
fun CameraManager.getAvailableCameraTypes(): List<String> {
|
|
||||||
val cameras = mutableListOf<String>()
|
|
||||||
try {
|
|
||||||
for (cameraId in cameraIdList) {
|
|
||||||
val characteristics = getCameraCharacteristics(cameraId)
|
|
||||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
|
||||||
|
|
||||||
when (facing) {
|
|
||||||
CameraCharacteristics.LENS_FACING_BACK -> {
|
|
||||||
// Проверяем на широкоугольный и телеобъектив
|
|
||||||
val focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
|
||||||
if (focalLengths != null && focalLengths.isNotEmpty()) {
|
|
||||||
val minFocalLength = focalLengths.minOrNull() ?: 0f
|
|
||||||
val maxFocalLength = focalLengths.maxOrNull() ?: 0f
|
|
||||||
|
|
||||||
when {
|
|
||||||
minFocalLength < 2.8f -> cameras.add(Constants.CameraTypes.WIDE)
|
|
||||||
maxFocalLength > 5.5f -> cameras.add(Constants.CameraTypes.TELEPHOTO)
|
|
||||||
else -> cameras.add(Constants.CameraTypes.BACK)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cameras.add(Constants.CameraTypes.BACK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CameraCharacteristics.LENS_FACING_FRONT -> cameras.add(Constants.CameraTypes.FRONT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CameraExtensions", "Error getting cameras", e)
|
|
||||||
// Добавляем базовые камеры как fallback
|
|
||||||
cameras.add(Constants.CameraTypes.BACK)
|
|
||||||
cameras.add(Constants.CameraTypes.FRONT)
|
|
||||||
}
|
|
||||||
return cameras.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CameraManager.getCameraIdForType(cameraType: String): String? {
|
|
||||||
return try {
|
|
||||||
for (cameraId in cameraIdList) {
|
|
||||||
val characteristics = getCameraCharacteristics(cameraId)
|
|
||||||
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
|
|
||||||
|
|
||||||
when (cameraType) {
|
|
||||||
Constants.CameraTypes.FRONT -> {
|
|
||||||
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
|
|
||||||
return cameraId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Constants.CameraTypes.BACK,
|
|
||||||
Constants.CameraTypes.WIDE,
|
|
||||||
Constants.CameraTypes.TELEPHOTO -> {
|
|
||||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
|
||||||
// Для простоты используем первую найденную заднюю камеру
|
|
||||||
// В реальном проекте здесь была бы более сложная логика
|
|
||||||
return cameraId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CameraExtensions", "Error finding camera for type $cameraType", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compose расширения для Flow
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun <T> Flow<T>.collectAsEffect(
|
|
||||||
key: Any? = null,
|
|
||||||
action: suspend (T) -> Unit
|
|
||||||
) {
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
LaunchedEffect(key) {
|
|
||||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
collect(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Логирование
|
|
||||||
*/
|
|
||||||
object Logger {
|
|
||||||
private const val TAG = "GodEye"
|
|
||||||
|
|
||||||
fun d(message: String, tag: String = TAG) {
|
|
||||||
Log.d(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun e(message: String, throwable: Throwable? = null, tag: String = TAG) {
|
|
||||||
Log.e(tag, message, throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun i(message: String, tag: String = TAG) {
|
|
||||||
Log.i(tag, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun w(message: String, tag: String = TAG) {
|
|
||||||
Log.w(tag, message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/src/main/java/com/example/godeye/utils/Logger.kt
Normal file
64
app/src/main/java/com/example/godeye/utils/Logger.kt
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package com.example.godeye.utils
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
object Logger {
|
||||||
|
private const val TAG = "GodEye"
|
||||||
|
|
||||||
|
fun d(message: String) {
|
||||||
|
Log.d(TAG, "🔍 $message")
|
||||||
|
println("🔍 [DEBUG] $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun i(message: String) {
|
||||||
|
Log.i(TAG, "ℹ️ $message")
|
||||||
|
println("ℹ️ [INFO] $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun w(message: String) {
|
||||||
|
Log.w(TAG, "⚠️ $message")
|
||||||
|
println("⚠️ [WARN] $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun e(message: String, throwable: Throwable? = null) {
|
||||||
|
Log.e(TAG, "❌ $message", throwable)
|
||||||
|
println("❌ [ERROR] $message")
|
||||||
|
throwable?.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun step(stepName: String, message: String) {
|
||||||
|
Log.d(TAG, "📋 STEP [$stepName]: $message")
|
||||||
|
println("📋 STEP [$stepName]: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun socket(message: String) {
|
||||||
|
Log.d(TAG, "🔌 SOCKET: $message")
|
||||||
|
println("🔌 SOCKET: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connection(message: String) {
|
||||||
|
Log.d(TAG, "🌐 CONNECTION: $message")
|
||||||
|
println("🌐 CONNECTION: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registration(message: String) {
|
||||||
|
Log.d(TAG, "📱 REGISTRATION: $message")
|
||||||
|
println("📱 REGISTRATION: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun camera(message: String) {
|
||||||
|
Log.d(TAG, "📷 CAMERA: $message")
|
||||||
|
println("📷 CAMERA: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun network(message: String) {
|
||||||
|
Log.d(TAG, "🌍 NETWORK: $message")
|
||||||
|
println("🌍 NETWORK: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun error(step: String, message: String, throwable: Throwable? = null) {
|
||||||
|
Log.e(TAG, "💥 ERROR in [$step]: $message", throwable)
|
||||||
|
println("💥 ERROR in [$step]: $message")
|
||||||
|
throwable?.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
646
app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt
Normal file
646
app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
package com.example.godeye.webrtc
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.example.godeye.utils.Logger
|
||||||
|
import org.webrtc.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebRTCManager - обработка WebRTC соединений согласно ТЗ
|
||||||
|
* Архитектура: Socket.IO для сигнализации, WebRTC для P2P медиа-потоков
|
||||||
|
*/
|
||||||
|
class WebRTCManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||||
|
private val activePeerConnections = mutableMapOf<String, PeerConnection>()
|
||||||
|
private var localVideoTrack: VideoTrack? = null
|
||||||
|
private var localAudioTrack: AudioTrack? = null
|
||||||
|
private var videoCapturer: CameraVideoCapturer? = null
|
||||||
|
|
||||||
|
// Состояния соединения
|
||||||
|
private val _connectionState = MutableStateFlow<Map<String, PeerConnection.PeerConnectionState>>(emptyMap())
|
||||||
|
val connectionState: StateFlow<Map<String, PeerConnection.PeerConnectionState>> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
|
// ICE серверы согласно ТЗ
|
||||||
|
private val iceServers = listOf(
|
||||||
|
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
|
||||||
|
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Конфигурация RTCConfiguration согласно ТЗ
|
||||||
|
private val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||||
|
tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED
|
||||||
|
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||||
|
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
|
||||||
|
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Logger.step("WEBRTC_INIT", "Initializing WebRTC Manager according to ТЗ")
|
||||||
|
initializePeerConnectionFactory()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация PeerConnectionFactory согласно ТЗ
|
||||||
|
*/
|
||||||
|
private fun initializePeerConnectionFactory() {
|
||||||
|
Logger.step("WEBRTC_FACTORY_INIT", "Initializing PeerConnectionFactory")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context)
|
||||||
|
.setEnableInternalTracer(false)
|
||||||
|
.createInitializationOptions()
|
||||||
|
PeerConnectionFactory.initialize(initializationOptions)
|
||||||
|
|
||||||
|
val options = PeerConnectionFactory.Options()
|
||||||
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
|
.setOptions(options)
|
||||||
|
.createPeerConnectionFactory()
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_FACTORY_READY", "PeerConnectionFactory initialized successfully")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_FACTORY_ERROR", "Failed to initialize PeerConnectionFactory", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начало стриминга для сессии - создание offer согласно ТЗ
|
||||||
|
*/
|
||||||
|
fun startStreaming(sessionId: String, cameraType: String) {
|
||||||
|
Logger.step("WEBRTC_START_STREAMING", "Starting WebRTC streaming for session: $sessionId, camera: $cameraType")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val peerConnection = createPeerConnection(sessionId)
|
||||||
|
if (peerConnection == null) {
|
||||||
|
Logger.error("WEBRTC_START_ERROR", "Failed to create peer connection", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление локальных медиа-потоков
|
||||||
|
addLocalStreams(peerConnection, cameraType)
|
||||||
|
|
||||||
|
// Создание offer согласно ТЗ
|
||||||
|
createOffer(sessionId, peerConnection)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание PeerConnection для сессии
|
||||||
|
*/
|
||||||
|
private fun createPeerConnection(sessionId: String): PeerConnection? {
|
||||||
|
val factory = peerConnectionFactory ?: return null
|
||||||
|
|
||||||
|
val observer = object : PeerConnection.Observer {
|
||||||
|
override fun onIceCandidate(candidate: IceCandidate) {
|
||||||
|
Logger.step("WEBRTC_ICE_CANDIDATE", "ICE candidate for session: $sessionId")
|
||||||
|
|
||||||
|
// Отправка ICE candidate через SocketService согласно ТЗ
|
||||||
|
val message = JSONObject().apply {
|
||||||
|
put("type", "ice-candidate")
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("candidate", candidate.sdp)
|
||||||
|
put("sdpMid", candidate.sdpMid)
|
||||||
|
put("sdpMLineIndex", candidate.sdpMLineIndex)
|
||||||
|
}
|
||||||
|
onSignalingMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
|
||||||
|
Logger.step("WEBRTC_CONNECTION_CHANGE", "Session $sessionId state: $newState")
|
||||||
|
|
||||||
|
val currentStates = _connectionState.value.toMutableMap()
|
||||||
|
currentStates[sessionId] = newState
|
||||||
|
_connectionState.value = currentStates
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||||
|
Logger.step("WEBRTC_ICE_CONNECTION", "Session $sessionId ICE state: $newState")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceConnectionReceivingChange(receiving: Boolean) {
|
||||||
|
Logger.step("WEBRTC_ICE_RECEIVING", "Session $sessionId ICE receiving: $receiving")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddStream(stream: MediaStream) {
|
||||||
|
Logger.step("WEBRTC_STREAM_ADDED", "Remote stream added for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveStream(stream: MediaStream) {
|
||||||
|
Logger.step("WEBRTC_STREAM_REMOVED", "Remote stream removed for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDataChannel(dataChannel: DataChannel) {
|
||||||
|
Logger.step("WEBRTC_DATA_CHANNEL", "Data channel opened for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRenegotiationNeeded() {
|
||||||
|
Logger.step("WEBRTC_RENEGOTIATION", "Renegotiation needed for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
|
||||||
|
Logger.step("WEBRTC_ICE_GATHERING", "Session $sessionId ICE gathering: $newState")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
|
||||||
|
Logger.step("WEBRTC_ICE_REMOVED", "ICE candidates removed for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
|
||||||
|
Logger.step("WEBRTC_SIGNALING", "Session $sessionId signaling: $newState")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val peerConnection = factory.createPeerConnection(rtcConfig, observer)
|
||||||
|
if (peerConnection != null) {
|
||||||
|
activePeerConnections[sessionId] = peerConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавление локальных медиа-потоков (видео + аудио)
|
||||||
|
*/
|
||||||
|
private fun addLocalStreams(peerConnection: PeerConnection, cameraType: String) {
|
||||||
|
try {
|
||||||
|
// Создание локального видео-потока
|
||||||
|
if (localVideoTrack == null) {
|
||||||
|
localVideoTrack = createVideoTrack(cameraType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание локального аудио-потока
|
||||||
|
if (localAudioTrack == null) {
|
||||||
|
localAudioTrack = createAudioTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавление потоков в PeerConnection
|
||||||
|
localVideoTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
|
||||||
|
localAudioTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_STREAMS_ADDED", "Local media streams added to peer connection")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_STREAMS_ERROR", "Failed to add local streams", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание видео-трека с указанной камерой
|
||||||
|
*/
|
||||||
|
private fun createVideoTrack(cameraType: String): VideoTrack? {
|
||||||
|
Logger.step("WEBRTC_VIDEO_TRACK", "Creating video track for camera: $cameraType")
|
||||||
|
|
||||||
|
try {
|
||||||
|
val factory = peerConnectionFactory ?: return null
|
||||||
|
|
||||||
|
// Создание видео источника
|
||||||
|
val videoSource = factory.createVideoSource(false)
|
||||||
|
|
||||||
|
// Создание захватчика камеры
|
||||||
|
videoCapturer = createCameraCapturer(cameraType)
|
||||||
|
|
||||||
|
if (videoCapturer == null) {
|
||||||
|
Logger.error("WEBRTC_VIDEO_ERROR", "Failed to create camera capturer", null)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация захвата видео
|
||||||
|
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", null)
|
||||||
|
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||||||
|
|
||||||
|
// Запуск захвата видео с разрешением 1280x720 и 30 FPS
|
||||||
|
videoCapturer?.startCapture(1280, 720, 30)
|
||||||
|
|
||||||
|
// Создание видео-трека
|
||||||
|
val videoTrack = factory.createVideoTrack("video_track", videoSource)
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_VIDEO_TRACK_CREATED", "Video track created successfully for camera: $cameraType")
|
||||||
|
return videoTrack
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_VIDEO_TRACK_ERROR", "Failed to create video track", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание захватчика камеры для указанного типа
|
||||||
|
*/
|
||||||
|
private fun createCameraCapturer(cameraType: String): CameraVideoCapturer? {
|
||||||
|
try {
|
||||||
|
val camera1Enumerator = Camera1Enumerator(false)
|
||||||
|
val camera2Enumerator = Camera2Enumerator(context)
|
||||||
|
|
||||||
|
val enumerator = if (Camera2Enumerator.isSupported(context)) camera2Enumerator else camera1Enumerator
|
||||||
|
|
||||||
|
// Поиск камеры по типу
|
||||||
|
val deviceNames = enumerator.deviceNames
|
||||||
|
for (deviceName in deviceNames) {
|
||||||
|
val isFrontFacing = enumerator.isFrontFacing(deviceName)
|
||||||
|
val isBackFacing = enumerator.isBackFacing(deviceName)
|
||||||
|
|
||||||
|
val matches = when (cameraType) {
|
||||||
|
"front" -> isFrontFacing
|
||||||
|
"back", "ultra_wide", "telephoto" -> isBackFacing
|
||||||
|
else -> isBackFacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
Logger.d("Using camera device: $deviceName for type: $cameraType")
|
||||||
|
return enumerator.createCapturer(deviceName, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback к первой доступной камере
|
||||||
|
if (deviceNames.isNotEmpty()) {
|
||||||
|
Logger.d("Using fallback camera: ${deviceNames[0]}")
|
||||||
|
return enumerator.createCapturer(deviceNames[0], null)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.error("CAMERA_CAPTURER_ERROR", "No camera devices found", null)
|
||||||
|
return null
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("CAMERA_CAPTURER_ERROR", "Failed to create camera capturer", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание аудио-трека
|
||||||
|
*/
|
||||||
|
private fun createAudioTrack(): AudioTrack? {
|
||||||
|
try {
|
||||||
|
val factory = peerConnectionFactory ?: return null
|
||||||
|
|
||||||
|
// Создание аудио источника
|
||||||
|
val audioConstraints = MediaConstraints().apply {
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioSource = factory.createAudioSource(audioConstraints)
|
||||||
|
val audioTrack = factory.createAudioTrack("audio_track", audioSource)
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_AUDIO_TRACK_CREATED", "Audio track created successfully")
|
||||||
|
return audioTrack
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_AUDIO_TRACK_ERROR", "Failed to create audio track", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создание offer для сессии
|
||||||
|
*/
|
||||||
|
private fun createOffer(sessionId: String, peerConnection: PeerConnection) {
|
||||||
|
try {
|
||||||
|
val constraints = MediaConstraints().apply {
|
||||||
|
// Исправляем настройки для корректной работы WebRTC
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
|
||||||
|
// Добавляем дополнительные constraints для стабильности
|
||||||
|
mandatory.add(MediaConstraints.KeyValuePair("VoiceActivityDetection", "true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.createOffer(object : SdpObserver {
|
||||||
|
override fun onCreateSuccess(sessionDescription: SessionDescription) {
|
||||||
|
Logger.step("WEBRTC_OFFER_CREATED", "Offer created for session: $sessionId")
|
||||||
|
|
||||||
|
// Изменяем SDP для исправления проблемы с m-section
|
||||||
|
val modifiedSdp = modifySdpForCompatibility(sessionDescription.description)
|
||||||
|
val modifiedSessionDescription = SessionDescription(sessionDescription.type, modifiedSdp)
|
||||||
|
|
||||||
|
peerConnection.setLocalDescription(object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
Logger.step("WEBRTC_LOCAL_DESC_SET", "Local description set successfully for session: $sessionId")
|
||||||
|
|
||||||
|
// Отправка offer через SocketService согласно ТЗ
|
||||||
|
val message = JSONObject().apply {
|
||||||
|
put("type", "offer")
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("sdp", modifiedSdp)
|
||||||
|
}
|
||||||
|
onSignalingMessage(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetFailure(error: String) {
|
||||||
|
Logger.error("WEBRTC_SET_LOCAL_ERROR", "Failed to set local description: $error", null)
|
||||||
|
// Не крашим приложение, а пытаемся создать новое соединение
|
||||||
|
handleWebRTCError(sessionId, "Local description error: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
}, modifiedSessionDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateFailure(error: String) {
|
||||||
|
Logger.error("WEBRTC_OFFER_ERROR", "Failed to create offer: $error", null)
|
||||||
|
handleWebRTCError(sessionId, "Offer creation error: $error")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {}
|
||||||
|
override fun onSetFailure(error: String?) {}
|
||||||
|
}, constraints)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_OFFER_EXCEPTION", "Exception creating offer", e)
|
||||||
|
handleWebRTCError(sessionId, "Offer exception: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Модификация SDP для совместимости
|
||||||
|
*/
|
||||||
|
private fun modifySdpForCompatibility(originalSdp: String): String {
|
||||||
|
try {
|
||||||
|
Logger.d("Original SDP length: ${originalSdp.length}")
|
||||||
|
|
||||||
|
// Для send-only режима создаем минимальный корректный SDP
|
||||||
|
val lines = originalSdp.split("\r\n").toMutableList()
|
||||||
|
val modifiedLines = mutableListOf<String>()
|
||||||
|
|
||||||
|
var inVideoSection = false
|
||||||
|
var inAudioSection = false
|
||||||
|
var currentSection = ""
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
when {
|
||||||
|
line.startsWith("v=") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("o=") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("s=") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("t=") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("a=group:BUNDLE") -> {
|
||||||
|
modifiedLines.add("a=group:BUNDLE 0 1")
|
||||||
|
}
|
||||||
|
line.startsWith("a=msid-semantic") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("m=video") -> {
|
||||||
|
inVideoSection = true
|
||||||
|
inAudioSection = false
|
||||||
|
currentSection = "video"
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("m=audio") -> {
|
||||||
|
inVideoSection = false
|
||||||
|
inAudioSection = true
|
||||||
|
currentSection = "audio"
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("c=") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("a=mid:") -> {
|
||||||
|
when (currentSection) {
|
||||||
|
"video" -> modifiedLines.add("a=mid:0")
|
||||||
|
"audio" -> modifiedLines.add("a=mid:1")
|
||||||
|
else -> modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.startsWith("a=sendonly") || line.startsWith("a=sendrecv") || line.startsWith("a=recvonly") -> {
|
||||||
|
modifiedLines.add("a=sendonly")
|
||||||
|
}
|
||||||
|
line.startsWith("a=rtcp-mux") && !line.contains("only") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
line.startsWith("a=rtpmap:") ||
|
||||||
|
line.startsWith("a=fmtp:") ||
|
||||||
|
line.startsWith("a=ssrc:") ||
|
||||||
|
line.startsWith("a=msid:") ||
|
||||||
|
line.startsWith("a=cname:") ||
|
||||||
|
line.startsWith("a=ice-ufrag:") ||
|
||||||
|
line.startsWith("a=ice-pwd:") ||
|
||||||
|
line.startsWith("a=fingerprint:") ||
|
||||||
|
line.startsWith("a=setup:") ||
|
||||||
|
line.startsWith("a=candidate:") -> {
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
// Пропускаем проблемные RTCP feedback атрибуты для send-only
|
||||||
|
line.startsWith("a=rtcp-fb:") ||
|
||||||
|
line.startsWith("a=rtcp-mux-only") ||
|
||||||
|
line.startsWith("a=rtcp-rsize") -> {
|
||||||
|
// Пропускаем эти строки
|
||||||
|
}
|
||||||
|
line.trim().isEmpty() -> {
|
||||||
|
// Пропускаем пустые строки
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Добавляем остальные атрибуты
|
||||||
|
modifiedLines.add(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val modifiedSdp = modifiedLines.joinToString("\r\n")
|
||||||
|
|
||||||
|
Logger.d("Modified SDP length: ${modifiedSdp.length}")
|
||||||
|
Logger.d("SDP modifications applied successfully")
|
||||||
|
|
||||||
|
return modifiedSdp
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("SDP_MODIFY_ERROR", "Failed to modify SDP", e)
|
||||||
|
return originalSdp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка WebRTC ошибок без краша приложения
|
||||||
|
*/
|
||||||
|
private fun handleWebRTCError(sessionId: String, error: String) {
|
||||||
|
Logger.error("WEBRTC_ERROR_HANDLED", "WebRTC error for session $sessionId: $error", null)
|
||||||
|
|
||||||
|
// Уведомляем о проблеме через сигналинг
|
||||||
|
val errorMessage = JSONObject().apply {
|
||||||
|
put("type", "error")
|
||||||
|
put("sessionId", sessionId)
|
||||||
|
put("error", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
onSignalingMessage(errorMessage)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_ERROR_SIGNALING", "Failed to send error message", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка answer от оператора
|
||||||
|
*/
|
||||||
|
fun handleAnswer(sessionId: String, answerSdp: String) {
|
||||||
|
Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: $sessionId")
|
||||||
|
|
||||||
|
val peerConnection = activePeerConnections[sessionId]
|
||||||
|
if (peerConnection == null) {
|
||||||
|
Logger.error("WEBRTC_ANSWER_ERROR", "No peer connection found for session: $sessionId", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val answer = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
|
||||||
|
peerConnection.setRemoteDescription(object : SdpObserver {
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
Logger.step("WEBRTC_ANSWER_SET", "Answer set successfully for session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetFailure(error: String) {
|
||||||
|
Logger.error("WEBRTC_ANSWER_SET_ERROR", "Failed to set answer: $error", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||||
|
override fun onCreateFailure(p0: String?) {}
|
||||||
|
}, answer)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_ANSWER_EXCEPTION", "Exception handling answer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка offer от оператора (не используется в текущей архитектуре)
|
||||||
|
*/
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun handleOffer(sessionId: String, offerSdp: String) {
|
||||||
|
Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: $sessionId")
|
||||||
|
// Пока не реализовано - Android устройство только отправляет offer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка ICE кандидата от оператора
|
||||||
|
*/
|
||||||
|
fun handleIceCandidate(sessionId: String, candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) {
|
||||||
|
Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: $sessionId")
|
||||||
|
|
||||||
|
val peerConnection = activePeerConnections[sessionId]
|
||||||
|
if (peerConnection == null) {
|
||||||
|
Logger.error("WEBRTC_ICE_ERROR", "No peer connection found for session: $sessionId", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidateSdp)
|
||||||
|
peerConnection.addIceCandidate(iceCandidate)
|
||||||
|
Logger.step("WEBRTC_ICE_ADDED", "ICE candidate added for session: $sessionId")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_ICE_EXCEPTION", "Exception handling ICE candidate", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключение камеры
|
||||||
|
*/
|
||||||
|
fun switchCamera(cameraType: String) {
|
||||||
|
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: $cameraType")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Остановка текущего захвата
|
||||||
|
videoCapturer?.stopCapture()
|
||||||
|
|
||||||
|
// Создание нового видео-трека
|
||||||
|
localVideoTrack?.dispose()
|
||||||
|
localVideoTrack = createVideoTrack(cameraType)
|
||||||
|
|
||||||
|
// Обновление треков во всех активных соединениях
|
||||||
|
activePeerConnections.forEach { (_, peerConnection) ->
|
||||||
|
localVideoTrack?.let { videoTrack ->
|
||||||
|
// Удаление старого трека и добавление нового
|
||||||
|
val senders = peerConnection.senders
|
||||||
|
senders.forEach { sender ->
|
||||||
|
if (sender.track()?.kind() == "video") {
|
||||||
|
sender.setTrack(videoTrack, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.step("WEBRTC_CAMERA_SWITCHED", "Camera switched to: $cameraType")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.error("WEBRTC_SWITCH_ERROR", "Failed to switch camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Завершение сессии
|
||||||
|
*/
|
||||||
|
fun endSession(sessionId: String) {
|
||||||
|
Logger.step("WEBRTC_END_SESSION", "Ending WebRTC session: $sessionId")
|
||||||
|
|
||||||
|
activePeerConnections[sessionId]?.let { peerConnection ->
|
||||||
|
peerConnection.close()
|
||||||
|
activePeerConnections.remove(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentStates = _connectionState.value.toMutableMap()
|
||||||
|
currentStates.remove(sessionId)
|
||||||
|
_connectionState.value = currentStates
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Остановка всех стримов
|
||||||
|
*/
|
||||||
|
fun stopAllStreaming() {
|
||||||
|
Logger.step("WEBRTC_STOP_ALL", "Stopping all WebRTC streaming")
|
||||||
|
|
||||||
|
activePeerConnections.forEach { (_, peerConnection) ->
|
||||||
|
peerConnection.close()
|
||||||
|
}
|
||||||
|
activePeerConnections.clear()
|
||||||
|
|
||||||
|
videoCapturer?.stopCapture()
|
||||||
|
videoCapturer?.dispose()
|
||||||
|
videoCapturer = null
|
||||||
|
|
||||||
|
localVideoTrack?.dispose()
|
||||||
|
localVideoTrack = null
|
||||||
|
|
||||||
|
localAudioTrack?.dispose()
|
||||||
|
localAudioTrack = null
|
||||||
|
|
||||||
|
_connectionState.value = emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Освобождение ресурсов
|
||||||
|
*/
|
||||||
|
fun dispose() {
|
||||||
|
Logger.step("WEBRTC_DISPOSE", "Disposing WebRTC Manager")
|
||||||
|
stopAllStreaming()
|
||||||
|
peerConnectionFactory?.dispose()
|
||||||
|
peerConnectionFactory = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SdpObserver по умолчанию для упрощения кода
|
||||||
|
*/
|
||||||
|
open class SimpleSdpObserver : SdpObserver {
|
||||||
|
override fun onCreateSuccess(sessionDescription: SessionDescription) {}
|
||||||
|
override fun onSetSuccess() {}
|
||||||
|
override fun onCreateFailure(error: String) {}
|
||||||
|
override fun onSetFailure(error: String) {}
|
||||||
|
}
|
||||||
16
app/src/main/res/drawable/circle_button_background.xml
Normal file
16
app/src/main/res/drawable/circle_button_background.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid android:color="#80FFFFFF" />
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#FFFFFF" />
|
||||||
|
|
||||||
|
<size
|
||||||
|
android:width="80dp"
|
||||||
|
android:height="80dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
|
|
||||||
100
app/src/main/res/layout/activity_legacy_camera.xml
Normal file
100
app/src/main/res/layout/activity_legacy_camera.xml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#000000">
|
||||||
|
|
||||||
|
<!-- Предварительный просмотр камеры -->
|
||||||
|
<SurfaceView
|
||||||
|
android:id="@+id/surfaceViewCamera"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
|
||||||
|
<!-- Верхняя панель с информацией -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/topPanel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCameraInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="📹 Legacy Camera для Android 9"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⚪ Инициализация..."
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Нижняя панель с кнопками управления -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottomPanel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Кнопка назад -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnBack"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="◀"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:background="@drawable/circle_button_background"
|
||||||
|
android:layout_marginEnd="24dp" />
|
||||||
|
|
||||||
|
<!-- Кнопка захвата -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnCapture"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:text="📷"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:background="@drawable/circle_button_background"
|
||||||
|
android:layout_marginHorizontal="24dp" />
|
||||||
|
|
||||||
|
<!-- Кнопка переключения камеры -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSwitchCamera"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="🔄"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:background="@drawable/circle_button_background"
|
||||||
|
android:layout_marginStart="24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Индикатор загрузки (скрыт по умолчанию) -->
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
247
app/src/main/res/layout/activity_legacy_main.xml
Normal file
247
app/src/main/res/layout/activity_legacy_main.xml
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:background="#F5F5F5">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<!-- Заголовок приложения -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="GodEye Legacy"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#2196F3"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
|
<!-- Карточка статуса -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Статус системы"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="❌ Сервис не подключен"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvConnectionStatus"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⚪ Не подключено"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPermissions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⚠️ Требуются разрешения"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<!-- Карточка информации об устройстве -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Информация об устройстве"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDeviceInfo"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="📱 Загрузка..."
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<!-- Карточка с кнопками управления -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Управление"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Кнопки подключения -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnConnect"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Подключиться"
|
||||||
|
android:backgroundTint="#4CAF50"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:enabled="false" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDisconnect"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Отключиться"
|
||||||
|
android:backgroundTint="#F44336"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Кнопки функций -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnCamera"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="📹 Камера"
|
||||||
|
android:backgroundTint="#2196F3"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:enabled="false" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSettings"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="⚙️ Настройки"
|
||||||
|
android:backgroundTint="#9C27B0"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<!-- Карточка с логами (для отладки) -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Информация для разработчика"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• Версия для Android 9+"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• Использует классические Views"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="• Совместимость с legacy устройствами"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#666666" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
@@ -1,80 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">GodEye Signal Center</string>
|
<string name="app_name">GodEye Signal Center</string>
|
||||||
|
<string name="connection_status">Статус подключения</string>
|
||||||
<!-- Главный экран -->
|
<string name="connect">Подключиться</string>
|
||||||
<string name="device_id_label">ID устройства:</string>
|
<string name="disconnect">Отключиться</string>
|
||||||
<string name="server_url_label">URL сервера:</string>
|
<string name="server_url">URL сервера</string>
|
||||||
<string name="server_url_hint">http://192.168.1.100:3001</string>
|
<string name="device_info">Информация об устройстве</string>
|
||||||
<string name="connection_status_label">Статус подключения:</string>
|
|
||||||
<string name="connect_button">Подключиться</string>
|
|
||||||
<string name="disconnect_button">Отключиться</string>
|
|
||||||
<string name="active_sessions_label">Активные сессии:</string>
|
|
||||||
<string name="no_active_sessions">Нет активных сессий</string>
|
|
||||||
|
|
||||||
<!-- Статусы подключения -->
|
|
||||||
<string name="status_disconnected">Отключено</string>
|
|
||||||
<string name="status_connecting">Подключение...</string>
|
|
||||||
<string name="status_connected">Подключено</string>
|
|
||||||
<string name="status_error">Ошибка подключения</string>
|
|
||||||
<string name="status_reconnecting">Переподключение...</string>
|
|
||||||
|
|
||||||
<!-- Диалог запроса камеры -->
|
|
||||||
<string name="camera_request_title">Запрос доступа к камере</string>
|
<string name="camera_request_title">Запрос доступа к камере</string>
|
||||||
<string name="camera_request_message">Оператор %1$s запрашивает доступ к камере %2$s</string>
|
<string name="allow">Разрешить</string>
|
||||||
<string name="session_id_label">ID сессии: %1$s</string>
|
<string name="deny">Отклонить</string>
|
||||||
<string name="allow_button">Разрешить</string>
|
<string name="active_sessions">Активные сессии</string>
|
||||||
<string name="deny_button">Отклонить</string>
|
<string name="no_sessions">Нет активных сессий</string>
|
||||||
<string name="remember_choice">Запомнить для этого оператора</string>
|
<string name="waiting_for_requests">Ожидание запросов операторов</string>
|
||||||
|
<string name="device_ready">Устройство готово к приему сессий</string>
|
||||||
<!-- Типы камер -->
|
<string name="permissions_required">Требуются разрешения</string>
|
||||||
<string name="camera_type_back">Основная</string>
|
<string name="grant_permissions">Предоставить разрешения</string>
|
||||||
<string name="camera_type_front">Фронтальная</string>
|
|
||||||
<string name="camera_type_wide">Широкоугольная</string>
|
|
||||||
<string name="camera_type_telephoto">Телеобъектив</string>
|
|
||||||
|
|
||||||
<!-- Сессии -->
|
|
||||||
<string name="session_operator_label">Оператор:</string>
|
|
||||||
<string name="session_camera_label">Камера:</string>
|
|
||||||
<string name="session_duration_label">Длительность:</string>
|
|
||||||
<string name="session_webrtc_status">WebRTC: %1$s</string>
|
|
||||||
<string name="webrtc_connected">Подключено</string>
|
|
||||||
<string name="webrtc_disconnected">Отключено</string>
|
|
||||||
<string name="end_session_button">Завершить</string>
|
|
||||||
|
|
||||||
<!-- Ошибки -->
|
|
||||||
<string name="error_network">Ошибка сети</string>
|
|
||||||
<string name="error_camera_permission">Нет разрешения на камеру</string>
|
|
||||||
<string name="error_audio_permission">Нет разрешения на микрофон</string>
|
|
||||||
<string name="error_camera_not_available">Камера недоступна</string>
|
|
||||||
<string name="error_webrtc_connection_failed">Ошибка WebRTC соединения</string>
|
|
||||||
<string name="error_socket">Ошибка WebSocket: %1$s</string>
|
|
||||||
<string name="error_camera">Ошибка камеры: %1$s</string>
|
|
||||||
<string name="error_unknown">Неизвестная ошибка</string>
|
|
||||||
|
|
||||||
<!-- Разрешения -->
|
|
||||||
<string name="permissions_required_title">Необходимы разрешения</string>
|
|
||||||
<string name="permissions_required_message">Для работы приложения необходимы разрешения на камеру, микрофон и уведомления</string>
|
|
||||||
<string name="grant_permissions_button">Предоставить разрешения</string>
|
|
||||||
<string name="permissions_denied_message">Без разрешений приложение не может работать</string>
|
|
||||||
|
|
||||||
<!-- Уведомления -->
|
|
||||||
<string name="notification_service_title">GodEye Signal Center</string>
|
|
||||||
<string name="notification_service_connected">Подключено к серверу</string>
|
|
||||||
<string name="notification_service_disconnected">Отключено от сервера</string>
|
|
||||||
<string name="notification_camera_title">GodEye Camera</string>
|
|
||||||
<string name="notification_camera_active">Активных сессий: %1$d</string>
|
|
||||||
<string name="notification_camera_ready">Камера готова к работе</string>
|
|
||||||
|
|
||||||
<!-- Общие -->
|
|
||||||
<string name="ok">OK</string>
|
|
||||||
<string name="cancel">Отмена</string>
|
|
||||||
<string name="settings">Настройки</string>
|
|
||||||
<string name="loading">Загрузка...</string>
|
|
||||||
<string name="retry">Повторить</string>
|
|
||||||
|
|
||||||
<!-- Время -->
|
|
||||||
<string name="duration_format">%1$02d:%2$02d:%3$02d</string>
|
|
||||||
<string name="time_seconds">сек</string>
|
|
||||||
<string name="time_minutes">мин</string>
|
|
||||||
<string name="time_hours">ч</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<style name="Theme.GodEye" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<style name="Theme.GodEye" parent="android:Theme.Material.Light.NoActionBar" />
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
14
app/src/main/res/xml/network_security_config.xml
Normal file
14
app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<network-security-config>
|
||||||
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
|
<domain includeSubdomains="true">192.168.219.108</domain>
|
||||||
|
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||||
|
<domain includeSubdomains="true">localhost</domain>
|
||||||
|
</domain-config>
|
||||||
|
<base-config cleartextTrafficPermitted="true">
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system"/>
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
</network-security-config>
|
||||||
|
|
||||||
87
install_to_lg_g6.sh
Executable file
87
install_to_lg_g6.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Автоматическая сборка, установка APK на LG G6 и мониторинг логов
|
||||||
|
# Использование: ./install_to_lg_g6.sh
|
||||||
|
|
||||||
|
echo "🔧 GodEye APK Builder & Installer для LG G6"
|
||||||
|
echo "=============================================="
|
||||||
|
|
||||||
|
APK_PATH="/home/trevor/AndroidStudioProjects/GodEye/app/build/outputs/apk/debug/app-debug.apk"
|
||||||
|
LG_G6_DEVICE="LGMG600S9b4da66b"
|
||||||
|
|
||||||
|
# Функция для логирования с временными метками
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%H:%M:%S')] $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем подключение LG G6
|
||||||
|
log "🔍 Проверяю подключение LG G6..."
|
||||||
|
if ! adb devices | grep -q "$LG_G6_DEVICE.*device"; then
|
||||||
|
echo "❌ LG G6 не найден!"
|
||||||
|
echo ""
|
||||||
|
echo "📱 Убедитесь что:"
|
||||||
|
echo "1. LG G6 подключен USB-кабелем"
|
||||||
|
echo "2. Включена отладка по USB"
|
||||||
|
echo "3. Разрешена отладка на устройстве"
|
||||||
|
echo ""
|
||||||
|
echo "Подключенные устройства:"
|
||||||
|
adb devices
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "✅ LG G6 найден: $LG_G6_DEVICE"
|
||||||
|
|
||||||
|
# Собираем проект
|
||||||
|
log "🔨 Собираю проект..."
|
||||||
|
if ! ./gradlew assembleDebug; then
|
||||||
|
echo "❌ Ошибка сборки проекта!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем существование APK файла
|
||||||
|
if [ ! -f "$APK_PATH" ]; then
|
||||||
|
echo "❌ APK файл не найден после сборки: $APK_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "✅ APK файл успешно собран: $(ls -lh $APK_PATH | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Устанавливаем APK на LG G6
|
||||||
|
log "📱 Устанавливаю APK на LG G6..."
|
||||||
|
if adb -s "$LG_G6_DEVICE" install -r "$APK_PATH"; then
|
||||||
|
log "✅ APK успешно установлен на LG G6!"
|
||||||
|
else
|
||||||
|
echo "❌ Ошибка установки APK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Очищаем логи
|
||||||
|
log "🧹 Очищаю старые логи..."
|
||||||
|
adb -s "$LG_G6_DEVICE" logcat -c
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
log "🚀 Запускаю приложение GodEye..."
|
||||||
|
adb -s "$LG_G6_DEVICE" shell am start -n com.example.godeye/.MainActivity
|
||||||
|
|
||||||
|
# Ждем немного для инициализации
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
log "📊 Запускаю мониторинг логов..."
|
||||||
|
log "==============================="
|
||||||
|
log "🔍 Отслеживаю все события приложения GodEye"
|
||||||
|
log "💡 Для остановки нажмите Ctrl+C"
|
||||||
|
log "==============================="
|
||||||
|
|
||||||
|
# Запускаем мониторинг логов с фильтрацией
|
||||||
|
adb -s "$LG_G6_DEVICE" logcat -s GodEye System.out | while read line; do
|
||||||
|
# Добавляем цветную индикацию важных событий
|
||||||
|
if echo "$line" | grep -q "STEP\|EVENT\|CAMERA\|SESSION\|CONNECT"; then
|
||||||
|
echo -e "\033[1;32m[$(date '+%H:%M:%S')] $line\033[0m"
|
||||||
|
elif echo "$line" | grep -q "ERROR"; then
|
||||||
|
echo -e "\033[1;31m[$(date '+%H:%M:%S')] $line\033[0m"
|
||||||
|
elif echo "$line" | grep -q "AUTO-ACCEPT"; then
|
||||||
|
echo -e "\033[1;33m[$(date '+%H:%M:%S')] $line\033[0m"
|
||||||
|
else
|
||||||
|
echo "[$(date '+%H:%M:%S')] $line"
|
||||||
|
fi
|
||||||
|
done
|
||||||
1
node_modules/.bin/mime
generated
vendored
1
node_modules/.bin/mime
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../mime/cli.js
|
|
||||||
1
node_modules/.bin/nodemon
generated
vendored
1
node_modules/.bin/nodemon
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../nodemon/bin/nodemon.js
|
|
||||||
1
node_modules/.bin/nodetouch
generated
vendored
1
node_modules/.bin/nodetouch
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../touch/bin/nodetouch.js
|
|
||||||
1
node_modules/.bin/semver
generated
vendored
1
node_modules/.bin/semver
generated
vendored
@@ -1 +0,0 @@
|
|||||||
../semver/bin/semver.js
|
|
||||||
1316
node_modules/.package-lock.json
generated
vendored
1316
node_modules/.package-lock.json
generated
vendored
File diff suppressed because it is too large
Load Diff
24
node_modules/@socket.io/component-emitter/LICENSE
generated
vendored
24
node_modules/@socket.io/component-emitter/LICENSE
generated
vendored
@@ -1,24 +0,0 @@
|
|||||||
(The MIT License)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Component contributors <dev@component.io>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
|
||||||
obtaining a copy of this software and associated documentation
|
|
||||||
files (the "Software"), to deal in the Software without
|
|
||||||
restriction, including without limitation the rights to use,
|
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
79
node_modules/@socket.io/component-emitter/Readme.md
generated
vendored
79
node_modules/@socket.io/component-emitter/Readme.md
generated
vendored
@@ -1,79 +0,0 @@
|
|||||||
# `@socket.io/component-emitter`
|
|
||||||
|
|
||||||
Event emitter component.
|
|
||||||
|
|
||||||
This project is a fork of the [`component-emitter`](https://github.com/sindresorhus/component-emitter) project, with [Socket.IO](https://socket.io/)-specific TypeScript typings.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
$ npm i @socket.io/component-emitter
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### Emitter(obj)
|
|
||||||
|
|
||||||
The `Emitter` may also be used as a mixin. For example
|
|
||||||
a "plain" object may become an emitter, or you may
|
|
||||||
extend an existing prototype.
|
|
||||||
|
|
||||||
As an `Emitter` instance:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { Emitter } from '@socket.io/component-emitter';
|
|
||||||
|
|
||||||
var emitter = new Emitter;
|
|
||||||
emitter.emit('something');
|
|
||||||
```
|
|
||||||
|
|
||||||
As a mixin:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { Emitter } from '@socket.io/component-emitter';
|
|
||||||
|
|
||||||
var user = { name: 'tobi' };
|
|
||||||
Emitter(user);
|
|
||||||
|
|
||||||
user.emit('im a user');
|
|
||||||
```
|
|
||||||
|
|
||||||
As a prototype mixin:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { Emitter } from '@socket.io/component-emitter';
|
|
||||||
|
|
||||||
Emitter(User.prototype);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Emitter#on(event, fn)
|
|
||||||
|
|
||||||
Register an `event` handler `fn`.
|
|
||||||
|
|
||||||
### Emitter#once(event, fn)
|
|
||||||
|
|
||||||
Register a single-shot `event` handler `fn`,
|
|
||||||
removed immediately after it is invoked the
|
|
||||||
first time.
|
|
||||||
|
|
||||||
### Emitter#off(event, fn)
|
|
||||||
|
|
||||||
* Pass `event` and `fn` to remove a listener.
|
|
||||||
* Pass `event` to remove all listeners on that event.
|
|
||||||
* Pass nothing to remove all listeners on all events.
|
|
||||||
|
|
||||||
### Emitter#emit(event, ...)
|
|
||||||
|
|
||||||
Emit an `event` with variable option args.
|
|
||||||
|
|
||||||
### Emitter#listeners(event)
|
|
||||||
|
|
||||||
Return an array of callbacks, or an empty array.
|
|
||||||
|
|
||||||
### Emitter#hasListeners(event)
|
|
||||||
|
|
||||||
Check if this emitter has `event` handlers.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
179
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
179
node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
generated
vendored
@@ -1,179 +0,0 @@
|
|||||||
/**
|
|
||||||
* An events map is an interface that maps event names to their value, which
|
|
||||||
* represents the type of the `on` listener.
|
|
||||||
*/
|
|
||||||
export interface EventsMap {
|
|
||||||
[event: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default events map, used if no EventsMap is given. Using this EventsMap
|
|
||||||
* is equivalent to accepting all event names, and any data.
|
|
||||||
*/
|
|
||||||
export interface DefaultEventsMap {
|
|
||||||
[event: string]: (...args: any[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a union type containing all the keys of an event map.
|
|
||||||
*/
|
|
||||||
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
|
|
||||||
|
|
||||||
/** The tuple type representing the parameters of an event listener */
|
|
||||||
export type EventParams<
|
|
||||||
Map extends EventsMap,
|
|
||||||
Ev extends EventNames<Map>
|
|
||||||
> = Parameters<Map[Ev]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The event names that are either in ReservedEvents or in UserEvents
|
|
||||||
*/
|
|
||||||
export type ReservedOrUserEventNames<
|
|
||||||
ReservedEventsMap extends EventsMap,
|
|
||||||
UserEvents extends EventsMap
|
|
||||||
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of a listener of a user event or a reserved event. If `Ev` is in
|
|
||||||
* `ReservedEvents`, the reserved event listener is returned.
|
|
||||||
*/
|
|
||||||
export type ReservedOrUserListener<
|
|
||||||
ReservedEvents extends EventsMap,
|
|
||||||
UserEvents extends EventsMap,
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
|
|
||||||
> = FallbackToUntypedListener<
|
|
||||||
Ev extends EventNames<ReservedEvents>
|
|
||||||
? ReservedEvents[Ev]
|
|
||||||
: Ev extends EventNames<UserEvents>
|
|
||||||
? UserEvents[Ev]
|
|
||||||
: never
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
|
|
||||||
*
|
|
||||||
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
|
|
||||||
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
|
|
||||||
*/
|
|
||||||
type FallbackToUntypedListener<T> = [T] extends [never]
|
|
||||||
? (...args: any[]) => void | Promise<void>
|
|
||||||
: T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
|
|
||||||
* parameters for mappings of event names to event data types, and strictly
|
|
||||||
* types method calls to the `EventEmitter` according to these event maps.
|
|
||||||
*
|
|
||||||
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
|
|
||||||
* listened to with `on` or `once`
|
|
||||||
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
|
|
||||||
* emitted with `emit`
|
|
||||||
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
|
|
||||||
* emitted by socket.io with `emitReserved`, and can be listened to with
|
|
||||||
* `listen`.
|
|
||||||
*/
|
|
||||||
export class Emitter<
|
|
||||||
ListenEvents extends EventsMap,
|
|
||||||
EmitEvents extends EventsMap,
|
|
||||||
ReservedEvents extends EventsMap = {}
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Adds the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a one-time `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev?: Ev,
|
|
||||||
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits an event.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param args Values to send to listeners of this event
|
|
||||||
*/
|
|
||||||
emit<Ev extends EventNames<EmitEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
...args: EventParams<EmitEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits a reserved event.
|
|
||||||
*
|
|
||||||
* This method is `protected`, so that only a class extending
|
|
||||||
* `StrictEventEmitter` can emit its own reserved events.
|
|
||||||
*
|
|
||||||
* @param ev Reserved event name
|
|
||||||
* @param args Arguments to emit along with the event
|
|
||||||
*/
|
|
||||||
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
...args: EventParams<ReservedEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the listeners listening to an event.
|
|
||||||
*
|
|
||||||
* @param event Event name
|
|
||||||
* @returns Array of listeners subscribed to `event`
|
|
||||||
*/
|
|
||||||
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
event: Ev
|
|
||||||
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if there is a listener for this event.
|
|
||||||
*
|
|
||||||
* @param event Event name
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
hasListeners<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(event: Ev): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
removeListener<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(
|
|
||||||
ev?: Ev,
|
|
||||||
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
*/
|
|
||||||
removeAllListeners<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(ev?: Ev): this;
|
|
||||||
}
|
|
||||||
176
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
176
node_modules/@socket.io/component-emitter/lib/cjs/index.js
generated
vendored
@@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* Expose `Emitter`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
exports.Emitter = Emitter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a new `Emitter`.
|
|
||||||
*
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Emitter(obj) {
|
|
||||||
if (obj) return mixin(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin the emitter properties.
|
|
||||||
*
|
|
||||||
* @param {Object} obj
|
|
||||||
* @return {Object}
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function mixin(obj) {
|
|
||||||
for (var key in Emitter.prototype) {
|
|
||||||
obj[key] = Emitter.prototype[key];
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen on the given `event` with `fn`.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.on =
|
|
||||||
Emitter.prototype.addEventListener = function(event, fn){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
(this._callbacks['$' + event] = this._callbacks['$' + event] || [])
|
|
||||||
.push(fn);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an `event` listener that will be invoked a single
|
|
||||||
* time then automatically removed.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.once = function(event, fn){
|
|
||||||
function on() {
|
|
||||||
this.off(event, on);
|
|
||||||
fn.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
on.fn = fn;
|
|
||||||
this.on(event, on);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the given callback for `event` or all
|
|
||||||
* registered callbacks.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.off =
|
|
||||||
Emitter.prototype.removeListener =
|
|
||||||
Emitter.prototype.removeAllListeners =
|
|
||||||
Emitter.prototype.removeEventListener = function(event, fn){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
|
|
||||||
// all
|
|
||||||
if (0 == arguments.length) {
|
|
||||||
this._callbacks = {};
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// specific event
|
|
||||||
var callbacks = this._callbacks['$' + event];
|
|
||||||
if (!callbacks) return this;
|
|
||||||
|
|
||||||
// remove all handlers
|
|
||||||
if (1 == arguments.length) {
|
|
||||||
delete this._callbacks['$' + event];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove specific handler
|
|
||||||
var cb;
|
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
|
||||||
cb = callbacks[i];
|
|
||||||
if (cb === fn || cb.fn === fn) {
|
|
||||||
callbacks.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove event specific arrays for event types that no
|
|
||||||
// one is subscribed for to avoid memory leak.
|
|
||||||
if (callbacks.length === 0) {
|
|
||||||
delete this._callbacks['$' + event];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit `event` with the given args.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Mixed} ...
|
|
||||||
* @return {Emitter}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.emit = function(event){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
|
|
||||||
var args = new Array(arguments.length - 1)
|
|
||||||
, callbacks = this._callbacks['$' + event];
|
|
||||||
|
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
|
||||||
args[i - 1] = arguments[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callbacks) {
|
|
||||||
callbacks = callbacks.slice(0);
|
|
||||||
for (var i = 0, len = callbacks.length; i < len; ++i) {
|
|
||||||
callbacks[i].apply(this, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// alias used for reserved events (protected method)
|
|
||||||
Emitter.prototype.emitReserved = Emitter.prototype.emit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return array of callbacks for `event`.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @return {Array}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.listeners = function(event){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
return this._callbacks['$' + event] || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this emitter has `event` handlers.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @return {Boolean}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.hasListeners = function(event){
|
|
||||||
return !! this.listeners(event).length;
|
|
||||||
};
|
|
||||||
4
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
4
node_modules/@socket.io/component-emitter/lib/cjs/package.json
generated
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@socket.io/component-emitter",
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
||||||
179
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
179
node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
generated
vendored
@@ -1,179 +0,0 @@
|
|||||||
/**
|
|
||||||
* An events map is an interface that maps event names to their value, which
|
|
||||||
* represents the type of the `on` listener.
|
|
||||||
*/
|
|
||||||
export interface EventsMap {
|
|
||||||
[event: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default events map, used if no EventsMap is given. Using this EventsMap
|
|
||||||
* is equivalent to accepting all event names, and any data.
|
|
||||||
*/
|
|
||||||
export interface DefaultEventsMap {
|
|
||||||
[event: string]: (...args: any[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a union type containing all the keys of an event map.
|
|
||||||
*/
|
|
||||||
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
|
|
||||||
|
|
||||||
/** The tuple type representing the parameters of an event listener */
|
|
||||||
export type EventParams<
|
|
||||||
Map extends EventsMap,
|
|
||||||
Ev extends EventNames<Map>
|
|
||||||
> = Parameters<Map[Ev]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The event names that are either in ReservedEvents or in UserEvents
|
|
||||||
*/
|
|
||||||
export type ReservedOrUserEventNames<
|
|
||||||
ReservedEventsMap extends EventsMap,
|
|
||||||
UserEvents extends EventsMap
|
|
||||||
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type of a listener of a user event or a reserved event. If `Ev` is in
|
|
||||||
* `ReservedEvents`, the reserved event listener is returned.
|
|
||||||
*/
|
|
||||||
export type ReservedOrUserListener<
|
|
||||||
ReservedEvents extends EventsMap,
|
|
||||||
UserEvents extends EventsMap,
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
|
|
||||||
> = FallbackToUntypedListener<
|
|
||||||
Ev extends EventNames<ReservedEvents>
|
|
||||||
? ReservedEvents[Ev]
|
|
||||||
: Ev extends EventNames<UserEvents>
|
|
||||||
? UserEvents[Ev]
|
|
||||||
: never
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
|
|
||||||
*
|
|
||||||
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
|
|
||||||
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
|
|
||||||
*/
|
|
||||||
type FallbackToUntypedListener<T> = [T] extends [never]
|
|
||||||
? (...args: any[]) => void | Promise<void>
|
|
||||||
: T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
|
|
||||||
* parameters for mappings of event names to event data types, and strictly
|
|
||||||
* types method calls to the `EventEmitter` according to these event maps.
|
|
||||||
*
|
|
||||||
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
|
|
||||||
* listened to with `on` or `once`
|
|
||||||
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
|
|
||||||
* emitted with `emit`
|
|
||||||
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
|
|
||||||
* emitted by socket.io with `emitReserved`, and can be listened to with
|
|
||||||
* `listen`.
|
|
||||||
*/
|
|
||||||
export class Emitter<
|
|
||||||
ListenEvents extends EventsMap,
|
|
||||||
EmitEvents extends EventsMap,
|
|
||||||
ReservedEvents extends EventsMap = {}
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Adds the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a one-time `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
ev?: Ev,
|
|
||||||
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits an event.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param args Values to send to listeners of this event
|
|
||||||
*/
|
|
||||||
emit<Ev extends EventNames<EmitEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
...args: EventParams<EmitEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits a reserved event.
|
|
||||||
*
|
|
||||||
* This method is `protected`, so that only a class extending
|
|
||||||
* `StrictEventEmitter` can emit its own reserved events.
|
|
||||||
*
|
|
||||||
* @param ev Reserved event name
|
|
||||||
* @param args Arguments to emit along with the event
|
|
||||||
*/
|
|
||||||
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
|
|
||||||
ev: Ev,
|
|
||||||
...args: EventParams<ReservedEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the listeners listening to an event.
|
|
||||||
*
|
|
||||||
* @param event Event name
|
|
||||||
* @returns Array of listeners subscribed to `event`
|
|
||||||
*/
|
|
||||||
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
|
|
||||||
event: Ev
|
|
||||||
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if there is a listener for this event.
|
|
||||||
*
|
|
||||||
* @param event Event name
|
|
||||||
* @returns boolean
|
|
||||||
*/
|
|
||||||
hasListeners<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(event: Ev): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
* @param listener Callback function
|
|
||||||
*/
|
|
||||||
removeListener<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(
|
|
||||||
ev?: Ev,
|
|
||||||
listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
|
|
||||||
): this;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all `listener` function as an event listener for `ev`.
|
|
||||||
*
|
|
||||||
* @param ev Name of the event
|
|
||||||
*/
|
|
||||||
removeAllListeners<
|
|
||||||
Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>
|
|
||||||
>(ev?: Ev): this;
|
|
||||||
}
|
|
||||||
169
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
169
node_modules/@socket.io/component-emitter/lib/esm/index.js
generated
vendored
@@ -1,169 +0,0 @@
|
|||||||
/**
|
|
||||||
* Initialize a new `Emitter`.
|
|
||||||
*
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function Emitter(obj) {
|
|
||||||
if (obj) return mixin(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin the emitter properties.
|
|
||||||
*
|
|
||||||
* @param {Object} obj
|
|
||||||
* @return {Object}
|
|
||||||
* @api private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function mixin(obj) {
|
|
||||||
for (var key in Emitter.prototype) {
|
|
||||||
obj[key] = Emitter.prototype[key];
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen on the given `event` with `fn`.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.on =
|
|
||||||
Emitter.prototype.addEventListener = function(event, fn){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
(this._callbacks['$' + event] = this._callbacks['$' + event] || [])
|
|
||||||
.push(fn);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds an `event` listener that will be invoked a single
|
|
||||||
* time then automatically removed.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.once = function(event, fn){
|
|
||||||
function on() {
|
|
||||||
this.off(event, on);
|
|
||||||
fn.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
on.fn = fn;
|
|
||||||
this.on(event, on);
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the given callback for `event` or all
|
|
||||||
* registered callbacks.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Function} fn
|
|
||||||
* @return {Emitter}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.off =
|
|
||||||
Emitter.prototype.removeListener =
|
|
||||||
Emitter.prototype.removeAllListeners =
|
|
||||||
Emitter.prototype.removeEventListener = function(event, fn){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
|
|
||||||
// all
|
|
||||||
if (0 == arguments.length) {
|
|
||||||
this._callbacks = {};
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// specific event
|
|
||||||
var callbacks = this._callbacks['$' + event];
|
|
||||||
if (!callbacks) return this;
|
|
||||||
|
|
||||||
// remove all handlers
|
|
||||||
if (1 == arguments.length) {
|
|
||||||
delete this._callbacks['$' + event];
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove specific handler
|
|
||||||
var cb;
|
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
|
||||||
cb = callbacks[i];
|
|
||||||
if (cb === fn || cb.fn === fn) {
|
|
||||||
callbacks.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove event specific arrays for event types that no
|
|
||||||
// one is subscribed for to avoid memory leak.
|
|
||||||
if (callbacks.length === 0) {
|
|
||||||
delete this._callbacks['$' + event];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit `event` with the given args.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Mixed} ...
|
|
||||||
* @return {Emitter}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.emit = function(event){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
|
|
||||||
var args = new Array(arguments.length - 1)
|
|
||||||
, callbacks = this._callbacks['$' + event];
|
|
||||||
|
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
|
||||||
args[i - 1] = arguments[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callbacks) {
|
|
||||||
callbacks = callbacks.slice(0);
|
|
||||||
for (var i = 0, len = callbacks.length; i < len; ++i) {
|
|
||||||
callbacks[i].apply(this, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// alias used for reserved events (protected method)
|
|
||||||
Emitter.prototype.emitReserved = Emitter.prototype.emit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return array of callbacks for `event`.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @return {Array}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.listeners = function(event){
|
|
||||||
this._callbacks = this._callbacks || {};
|
|
||||||
return this._callbacks['$' + event] || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this emitter has `event` handlers.
|
|
||||||
*
|
|
||||||
* @param {String} event
|
|
||||||
* @return {Boolean}
|
|
||||||
* @api public
|
|
||||||
*/
|
|
||||||
|
|
||||||
Emitter.prototype.hasListeners = function(event){
|
|
||||||
return !! this.listeners(event).length;
|
|
||||||
};
|
|
||||||
4
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
4
node_modules/@socket.io/component-emitter/lib/esm/package.json
generated
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@socket.io/component-emitter",
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
28
node_modules/@socket.io/component-emitter/package.json
generated
vendored
28
node_modules/@socket.io/component-emitter/package.json
generated
vendored
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@socket.io/component-emitter",
|
|
||||||
"description": "Event emitter",
|
|
||||||
"version": "3.1.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"mocha": "*",
|
|
||||||
"should": "*"
|
|
||||||
},
|
|
||||||
"component": {
|
|
||||||
"scripts": {
|
|
||||||
"emitter/index.js": "index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"main": "./lib/cjs/index.js",
|
|
||||||
"module": "./lib/esm/index.js",
|
|
||||||
"types": "./lib/cjs/index.d.ts",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/socketio/emitter.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "make test"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"lib/"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
21
node_modules/@types/cors/LICENSE
generated
vendored
21
node_modules/@types/cors/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE
|
|
||||||
75
node_modules/@types/cors/README.md
generated
vendored
75
node_modules/@types/cors/README.md
generated
vendored
@@ -1,75 +0,0 @@
|
|||||||
# Installation
|
|
||||||
> `npm install --save @types/cors`
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
This package contains type definitions for cors (https://github.com/expressjs/cors/).
|
|
||||||
|
|
||||||
# Details
|
|
||||||
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cors.
|
|
||||||
## [index.d.ts](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cors/index.d.ts)
|
|
||||||
````ts
|
|
||||||
/// <reference types="node" />
|
|
||||||
|
|
||||||
import { IncomingHttpHeaders } from "http";
|
|
||||||
|
|
||||||
type StaticOrigin = boolean | string | RegExp | Array<boolean | string | RegExp>;
|
|
||||||
|
|
||||||
type CustomOrigin = (
|
|
||||||
requestOrigin: string | undefined,
|
|
||||||
callback: (err: Error | null, origin?: StaticOrigin) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
declare namespace e {
|
|
||||||
interface CorsRequest {
|
|
||||||
method?: string | undefined;
|
|
||||||
headers: IncomingHttpHeaders;
|
|
||||||
}
|
|
||||||
interface CorsOptions {
|
|
||||||
/**
|
|
||||||
* @default '*'
|
|
||||||
*/
|
|
||||||
origin?: StaticOrigin | CustomOrigin | undefined;
|
|
||||||
/**
|
|
||||||
* @default 'GET,HEAD,PUT,PATCH,POST,DELETE'
|
|
||||||
*/
|
|
||||||
methods?: string | string[] | undefined;
|
|
||||||
allowedHeaders?: string | string[] | undefined;
|
|
||||||
exposedHeaders?: string | string[] | undefined;
|
|
||||||
credentials?: boolean | undefined;
|
|
||||||
maxAge?: number | undefined;
|
|
||||||
/**
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
preflightContinue?: boolean | undefined;
|
|
||||||
/**
|
|
||||||
* @default 204
|
|
||||||
*/
|
|
||||||
optionsSuccessStatus?: number | undefined;
|
|
||||||
}
|
|
||||||
type CorsOptionsDelegate<T extends CorsRequest = CorsRequest> = (
|
|
||||||
req: T,
|
|
||||||
callback: (err: Error | null, options?: CorsOptions) => void,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare function e<T extends e.CorsRequest = e.CorsRequest>(
|
|
||||||
options?: e.CorsOptions | e.CorsOptionsDelegate<T>,
|
|
||||||
): (
|
|
||||||
req: T,
|
|
||||||
res: {
|
|
||||||
statusCode?: number | undefined;
|
|
||||||
setHeader(key: string, value: string): any;
|
|
||||||
end(): any;
|
|
||||||
},
|
|
||||||
next: (err?: any) => any,
|
|
||||||
) => void;
|
|
||||||
export = e;
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
### Additional Details
|
|
||||||
* Last updated: Sat, 07 Jun 2025 02:15:25 GMT
|
|
||||||
* Dependencies: [@types/node](https://npmjs.com/package/@types/node)
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
These definitions were written by [Alan Plum](https://github.com/pluma), [Gaurav Sharma](https://github.com/gtpan77), and [Sebastian Beltran](https://github.com/bjohansebas).
|
|
||||||
56
node_modules/@types/cors/index.d.ts
generated
vendored
56
node_modules/@types/cors/index.d.ts
generated
vendored
@@ -1,56 +0,0 @@
|
|||||||
/// <reference types="node" />
|
|
||||||
|
|
||||||
import { IncomingHttpHeaders } from "http";
|
|
||||||
|
|
||||||
type StaticOrigin = boolean | string | RegExp | Array<boolean | string | RegExp>;
|
|
||||||
|
|
||||||
type CustomOrigin = (
|
|
||||||
requestOrigin: string | undefined,
|
|
||||||
callback: (err: Error | null, origin?: StaticOrigin) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
declare namespace e {
|
|
||||||
interface CorsRequest {
|
|
||||||
method?: string | undefined;
|
|
||||||
headers: IncomingHttpHeaders;
|
|
||||||
}
|
|
||||||
interface CorsOptions {
|
|
||||||
/**
|
|
||||||
* @default '*'
|
|
||||||
*/
|
|
||||||
origin?: StaticOrigin | CustomOrigin | undefined;
|
|
||||||
/**
|
|
||||||
* @default 'GET,HEAD,PUT,PATCH,POST,DELETE'
|
|
||||||
*/
|
|
||||||
methods?: string | string[] | undefined;
|
|
||||||
allowedHeaders?: string | string[] | undefined;
|
|
||||||
exposedHeaders?: string | string[] | undefined;
|
|
||||||
credentials?: boolean | undefined;
|
|
||||||
maxAge?: number | undefined;
|
|
||||||
/**
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
preflightContinue?: boolean | undefined;
|
|
||||||
/**
|
|
||||||
* @default 204
|
|
||||||
*/
|
|
||||||
optionsSuccessStatus?: number | undefined;
|
|
||||||
}
|
|
||||||
type CorsOptionsDelegate<T extends CorsRequest = CorsRequest> = (
|
|
||||||
req: T,
|
|
||||||
callback: (err: Error | null, options?: CorsOptions) => void,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare function e<T extends e.CorsRequest = e.CorsRequest>(
|
|
||||||
options?: e.CorsOptions | e.CorsOptionsDelegate<T>,
|
|
||||||
): (
|
|
||||||
req: T,
|
|
||||||
res: {
|
|
||||||
statusCode?: number | undefined;
|
|
||||||
setHeader(key: string, value: string): any;
|
|
||||||
end(): any;
|
|
||||||
},
|
|
||||||
next: (err?: any) => any,
|
|
||||||
) => void;
|
|
||||||
export = e;
|
|
||||||
38
node_modules/@types/cors/package.json
generated
vendored
38
node_modules/@types/cors/package.json
generated
vendored
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@types/cors",
|
|
||||||
"version": "2.8.19",
|
|
||||||
"description": "TypeScript definitions for cors",
|
|
||||||
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/cors",
|
|
||||||
"license": "MIT",
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Alan Plum",
|
|
||||||
"githubUsername": "pluma",
|
|
||||||
"url": "https://github.com/pluma"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gaurav Sharma",
|
|
||||||
"githubUsername": "gtpan77",
|
|
||||||
"url": "https://github.com/gtpan77"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Sebastian Beltran",
|
|
||||||
"githubUsername": "bjohansebas",
|
|
||||||
"url": "https://github.com/bjohansebas"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"main": "",
|
|
||||||
"types": "index.d.ts",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
|
|
||||||
"directory": "types/cors"
|
|
||||||
},
|
|
||||||
"scripts": {},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
},
|
|
||||||
"peerDependencies": {},
|
|
||||||
"typesPublisherContentHash": "a090e558c5f443573318c2955deecddc840bd8dfaac7cdedf31c7f6ede8d0b47",
|
|
||||||
"typeScriptVersion": "5.1"
|
|
||||||
}
|
|
||||||
21
node_modules/@types/node/LICENSE
generated
vendored
21
node_modules/@types/node/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) Microsoft Corporation.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE
|
|
||||||
15
node_modules/@types/node/README.md
generated
vendored
15
node_modules/@types/node/README.md
generated
vendored
@@ -1,15 +0,0 @@
|
|||||||
# Installation
|
|
||||||
> `npm install --save @types/node`
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
This package contains type definitions for node (https://nodejs.org/).
|
|
||||||
|
|
||||||
# Details
|
|
||||||
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node.
|
|
||||||
|
|
||||||
### Additional Details
|
|
||||||
* Last updated: Thu, 18 Sep 2025 00:04:03 GMT
|
|
||||||
* Dependencies: [undici-types](https://npmjs.com/package/undici-types)
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), [René](https://github.com/Renegade334), and [Yagiz Nizipli](https://github.com/anonrig).
|
|
||||||
1056
node_modules/@types/node/assert.d.ts
generated
vendored
1056
node_modules/@types/node/assert.d.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
8
node_modules/@types/node/assert/strict.d.ts
generated
vendored
8
node_modules/@types/node/assert/strict.d.ts
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
declare module "assert/strict" {
|
|
||||||
import { strict } from "node:assert";
|
|
||||||
export = strict;
|
|
||||||
}
|
|
||||||
declare module "node:assert/strict" {
|
|
||||||
import { strict } from "node:assert";
|
|
||||||
export = strict;
|
|
||||||
}
|
|
||||||
623
node_modules/@types/node/async_hooks.d.ts
generated
vendored
623
node_modules/@types/node/async_hooks.d.ts
generated
vendored
@@ -1,623 +0,0 @@
|
|||||||
/**
|
|
||||||
* We strongly discourage the use of the `async_hooks` API.
|
|
||||||
* Other APIs that can cover most of its use cases include:
|
|
||||||
*
|
|
||||||
* * [`AsyncLocalStorage`](https://nodejs.org/docs/latest-v24.x/api/async_context.html#class-asynclocalstorage) tracks async context
|
|
||||||
* * [`process.getActiveResourcesInfo()`](https://nodejs.org/docs/latest-v24.x/api/process.html#processgetactiveresourcesinfo) tracks active resources
|
|
||||||
*
|
|
||||||
* The `node:async_hooks` module provides an API to track asynchronous resources.
|
|
||||||
* It can be accessed using:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import async_hooks from 'node:async_hooks';
|
|
||||||
* ```
|
|
||||||
* @experimental
|
|
||||||
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/async_hooks.js)
|
|
||||||
*/
|
|
||||||
declare module "async_hooks" {
|
|
||||||
/**
|
|
||||||
* ```js
|
|
||||||
* import { executionAsyncId } from 'node:async_hooks';
|
|
||||||
* import fs from 'node:fs';
|
|
||||||
*
|
|
||||||
* console.log(executionAsyncId()); // 1 - bootstrap
|
|
||||||
* const path = '.';
|
|
||||||
* fs.open(path, 'r', (err, fd) => {
|
|
||||||
* console.log(executionAsyncId()); // 6 - open()
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The ID returned from `executionAsyncId()` is related to execution timing, not
|
|
||||||
* causality (which is covered by `triggerAsyncId()`):
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const server = net.createServer((conn) => {
|
|
||||||
* // Returns the ID of the server, not of the new connection, because the
|
|
||||||
* // callback runs in the execution scope of the server's MakeCallback().
|
|
||||||
* async_hooks.executionAsyncId();
|
|
||||||
*
|
|
||||||
* }).listen(port, () => {
|
|
||||||
* // Returns the ID of a TickObject (process.nextTick()) because all
|
|
||||||
* // callbacks passed to .listen() are wrapped in a nextTick().
|
|
||||||
* async_hooks.executionAsyncId();
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Promise contexts may not get precise `executionAsyncIds` by default.
|
|
||||||
* See the section on [promise execution tracking](https://nodejs.org/docs/latest-v24.x/api/async_hooks.html#promise-execution-tracking).
|
|
||||||
* @since v8.1.0
|
|
||||||
* @return The `asyncId` of the current execution context. Useful to track when something calls.
|
|
||||||
*/
|
|
||||||
function executionAsyncId(): number;
|
|
||||||
/**
|
|
||||||
* Resource objects returned by `executionAsyncResource()` are most often internal
|
|
||||||
* Node.js handle objects with undocumented APIs. Using any functions or properties
|
|
||||||
* on the object is likely to crash your application and should be avoided.
|
|
||||||
*
|
|
||||||
* Using `executionAsyncResource()` in the top-level execution context will
|
|
||||||
* return an empty object as there is no handle or request object to use,
|
|
||||||
* but having an object representing the top-level can be helpful.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { open } from 'node:fs';
|
|
||||||
* import { executionAsyncId, executionAsyncResource } from 'node:async_hooks';
|
|
||||||
*
|
|
||||||
* console.log(executionAsyncId(), executionAsyncResource()); // 1 {}
|
|
||||||
* open(new URL(import.meta.url), 'r', (err, fd) => {
|
|
||||||
* console.log(executionAsyncId(), executionAsyncResource()); // 7 FSReqWrap
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* This can be used to implement continuation local storage without the
|
|
||||||
* use of a tracking `Map` to store the metadata:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { createServer } from 'node:http';
|
|
||||||
* import {
|
|
||||||
* executionAsyncId,
|
|
||||||
* executionAsyncResource,
|
|
||||||
* createHook,
|
|
||||||
* } from 'node:async_hooks';
|
|
||||||
* const sym = Symbol('state'); // Private symbol to avoid pollution
|
|
||||||
*
|
|
||||||
* createHook({
|
|
||||||
* init(asyncId, type, triggerAsyncId, resource) {
|
|
||||||
* const cr = executionAsyncResource();
|
|
||||||
* if (cr) {
|
|
||||||
* resource[sym] = cr[sym];
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* }).enable();
|
|
||||||
*
|
|
||||||
* const server = createServer((req, res) => {
|
|
||||||
* executionAsyncResource()[sym] = { state: req.url };
|
|
||||||
* setTimeout(function() {
|
|
||||||
* res.end(JSON.stringify(executionAsyncResource()[sym]));
|
|
||||||
* }, 100);
|
|
||||||
* }).listen(3000);
|
|
||||||
* ```
|
|
||||||
* @since v13.9.0, v12.17.0
|
|
||||||
* @return The resource representing the current execution. Useful to store data within the resource.
|
|
||||||
*/
|
|
||||||
function executionAsyncResource(): object;
|
|
||||||
/**
|
|
||||||
* ```js
|
|
||||||
* const server = net.createServer((conn) => {
|
|
||||||
* // The resource that caused (or triggered) this callback to be called
|
|
||||||
* // was that of the new connection. Thus the return value of triggerAsyncId()
|
|
||||||
* // is the asyncId of "conn".
|
|
||||||
* async_hooks.triggerAsyncId();
|
|
||||||
*
|
|
||||||
* }).listen(port, () => {
|
|
||||||
* // Even though all callbacks passed to .listen() are wrapped in a nextTick()
|
|
||||||
* // the callback itself exists because the call to the server's .listen()
|
|
||||||
* // was made. So the return value would be the ID of the server.
|
|
||||||
* async_hooks.triggerAsyncId();
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Promise contexts may not get valid `triggerAsyncId`s by default. See
|
|
||||||
* the section on [promise execution tracking](https://nodejs.org/docs/latest-v24.x/api/async_hooks.html#promise-execution-tracking).
|
|
||||||
* @return The ID of the resource responsible for calling the callback that is currently being executed.
|
|
||||||
*/
|
|
||||||
function triggerAsyncId(): number;
|
|
||||||
interface HookCallbacks {
|
|
||||||
/**
|
|
||||||
* Called when a class is constructed that has the possibility to emit an asynchronous event.
|
|
||||||
* @param asyncId A unique ID for the async resource
|
|
||||||
* @param type The type of the async resource
|
|
||||||
* @param triggerAsyncId The unique ID of the async resource in whose execution context this async resource was created
|
|
||||||
* @param resource Reference to the resource representing the async operation, needs to be released during destroy
|
|
||||||
*/
|
|
||||||
init?(asyncId: number, type: string, triggerAsyncId: number, resource: object): void;
|
|
||||||
/**
|
|
||||||
* When an asynchronous operation is initiated or completes a callback is called to notify the user.
|
|
||||||
* The before callback is called just before said callback is executed.
|
|
||||||
* @param asyncId the unique identifier assigned to the resource about to execute the callback.
|
|
||||||
*/
|
|
||||||
before?(asyncId: number): void;
|
|
||||||
/**
|
|
||||||
* Called immediately after the callback specified in `before` is completed.
|
|
||||||
*
|
|
||||||
* If an uncaught exception occurs during execution of the callback, then `after` will run after the `'uncaughtException'` event is emitted or a `domain`'s handler runs.
|
|
||||||
* @param asyncId the unique identifier assigned to the resource which has executed the callback.
|
|
||||||
*/
|
|
||||||
after?(asyncId: number): void;
|
|
||||||
/**
|
|
||||||
* Called when a promise has resolve() called. This may not be in the same execution id
|
|
||||||
* as the promise itself.
|
|
||||||
* @param asyncId the unique id for the promise that was resolve()d.
|
|
||||||
*/
|
|
||||||
promiseResolve?(asyncId: number): void;
|
|
||||||
/**
|
|
||||||
* Called after the resource corresponding to asyncId is destroyed
|
|
||||||
* @param asyncId a unique ID for the async resource
|
|
||||||
*/
|
|
||||||
destroy?(asyncId: number): void;
|
|
||||||
}
|
|
||||||
interface AsyncHook {
|
|
||||||
/**
|
|
||||||
* Enable the callbacks for a given AsyncHook instance. If no callbacks are provided enabling is a noop.
|
|
||||||
*/
|
|
||||||
enable(): this;
|
|
||||||
/**
|
|
||||||
* Disable the callbacks for a given AsyncHook instance from the global pool of AsyncHook callbacks to be executed. Once a hook has been disabled it will not be called again until enabled.
|
|
||||||
*/
|
|
||||||
disable(): this;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Registers functions to be called for different lifetime events of each async
|
|
||||||
* operation.
|
|
||||||
*
|
|
||||||
* The callbacks `init()`/`before()`/`after()`/`destroy()` are called for the
|
|
||||||
* respective asynchronous event during a resource's lifetime.
|
|
||||||
*
|
|
||||||
* All callbacks are optional. For example, if only resource cleanup needs to
|
|
||||||
* be tracked, then only the `destroy` callback needs to be passed. The
|
|
||||||
* specifics of all functions that can be passed to `callbacks` is in the `Hook Callbacks` section.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { createHook } from 'node:async_hooks';
|
|
||||||
*
|
|
||||||
* const asyncHook = createHook({
|
|
||||||
* init(asyncId, type, triggerAsyncId, resource) { },
|
|
||||||
* destroy(asyncId) { },
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The callbacks will be inherited via the prototype chain:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* class MyAsyncCallbacks {
|
|
||||||
* init(asyncId, type, triggerAsyncId, resource) { }
|
|
||||||
* destroy(asyncId) {}
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* class MyAddedCallbacks extends MyAsyncCallbacks {
|
|
||||||
* before(asyncId) { }
|
|
||||||
* after(asyncId) { }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* const asyncHook = async_hooks.createHook(new MyAddedCallbacks());
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Because promises are asynchronous resources whose lifecycle is tracked
|
|
||||||
* via the async hooks mechanism, the `init()`, `before()`, `after()`, and`destroy()` callbacks _must not_ be async functions that return promises.
|
|
||||||
* @since v8.1.0
|
|
||||||
* @param callbacks The `Hook Callbacks` to register
|
|
||||||
* @return Instance used for disabling and enabling hooks
|
|
||||||
*/
|
|
||||||
function createHook(callbacks: HookCallbacks): AsyncHook;
|
|
||||||
interface AsyncResourceOptions {
|
|
||||||
/**
|
|
||||||
* The ID of the execution context that created this async event.
|
|
||||||
* @default executionAsyncId()
|
|
||||||
*/
|
|
||||||
triggerAsyncId?: number | undefined;
|
|
||||||
/**
|
|
||||||
* Disables automatic `emitDestroy` when the object is garbage collected.
|
|
||||||
* This usually does not need to be set (even if `emitDestroy` is called
|
|
||||||
* manually), unless the resource's `asyncId` is retrieved and the
|
|
||||||
* sensitive API's `emitDestroy` is called with it.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
requireManualDestroy?: boolean | undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The class `AsyncResource` is designed to be extended by the embedder's async
|
|
||||||
* resources. Using this, users can easily trigger the lifetime events of their
|
|
||||||
* own resources.
|
|
||||||
*
|
|
||||||
* The `init` hook will trigger when an `AsyncResource` is instantiated.
|
|
||||||
*
|
|
||||||
* The following is an overview of the `AsyncResource` API.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { AsyncResource, executionAsyncId } from 'node:async_hooks';
|
|
||||||
*
|
|
||||||
* // AsyncResource() is meant to be extended. Instantiating a
|
|
||||||
* // new AsyncResource() also triggers init. If triggerAsyncId is omitted then
|
|
||||||
* // async_hook.executionAsyncId() is used.
|
|
||||||
* const asyncResource = new AsyncResource(
|
|
||||||
* type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false },
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* // Run a function in the execution context of the resource. This will
|
|
||||||
* // * establish the context of the resource
|
|
||||||
* // * trigger the AsyncHooks before callbacks
|
|
||||||
* // * call the provided function `fn` with the supplied arguments
|
|
||||||
* // * trigger the AsyncHooks after callbacks
|
|
||||||
* // * restore the original execution context
|
|
||||||
* asyncResource.runInAsyncScope(fn, thisArg, ...args);
|
|
||||||
*
|
|
||||||
* // Call AsyncHooks destroy callbacks.
|
|
||||||
* asyncResource.emitDestroy();
|
|
||||||
*
|
|
||||||
* // Return the unique ID assigned to the AsyncResource instance.
|
|
||||||
* asyncResource.asyncId();
|
|
||||||
*
|
|
||||||
* // Return the trigger ID for the AsyncResource instance.
|
|
||||||
* asyncResource.triggerAsyncId();
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
class AsyncResource {
|
|
||||||
/**
|
|
||||||
* AsyncResource() is meant to be extended. Instantiating a
|
|
||||||
* new AsyncResource() also triggers init. If triggerAsyncId is omitted then
|
|
||||||
* async_hook.executionAsyncId() is used.
|
|
||||||
* @param type The type of async event.
|
|
||||||
* @param triggerAsyncId The ID of the execution context that created
|
|
||||||
* this async event (default: `executionAsyncId()`), or an
|
|
||||||
* AsyncResourceOptions object (since v9.3.0)
|
|
||||||
*/
|
|
||||||
constructor(type: string, triggerAsyncId?: number | AsyncResourceOptions);
|
|
||||||
/**
|
|
||||||
* Binds the given function to the current execution context.
|
|
||||||
* @since v14.8.0, v12.19.0
|
|
||||||
* @param fn The function to bind to the current execution context.
|
|
||||||
* @param type An optional name to associate with the underlying `AsyncResource`.
|
|
||||||
*/
|
|
||||||
static bind<Func extends (this: ThisArg, ...args: any[]) => any, ThisArg>(
|
|
||||||
fn: Func,
|
|
||||||
type?: string,
|
|
||||||
thisArg?: ThisArg,
|
|
||||||
): Func;
|
|
||||||
/**
|
|
||||||
* Binds the given function to execute to this `AsyncResource`'s scope.
|
|
||||||
* @since v14.8.0, v12.19.0
|
|
||||||
* @param fn The function to bind to the current `AsyncResource`.
|
|
||||||
*/
|
|
||||||
bind<Func extends (...args: any[]) => any>(fn: Func): Func;
|
|
||||||
/**
|
|
||||||
* Call the provided function with the provided arguments in the execution context
|
|
||||||
* of the async resource. This will establish the context, trigger the AsyncHooks
|
|
||||||
* before callbacks, call the function, trigger the AsyncHooks after callbacks, and
|
|
||||||
* then restore the original execution context.
|
|
||||||
* @since v9.6.0
|
|
||||||
* @param fn The function to call in the execution context of this async resource.
|
|
||||||
* @param thisArg The receiver to be used for the function call.
|
|
||||||
* @param args Optional arguments to pass to the function.
|
|
||||||
*/
|
|
||||||
runInAsyncScope<This, Result>(
|
|
||||||
fn: (this: This, ...args: any[]) => Result,
|
|
||||||
thisArg?: This,
|
|
||||||
...args: any[]
|
|
||||||
): Result;
|
|
||||||
/**
|
|
||||||
* Call all `destroy` hooks. This should only ever be called once. An error will
|
|
||||||
* be thrown if it is called more than once. This **must** be manually called. If
|
|
||||||
* the resource is left to be collected by the GC then the `destroy` hooks will
|
|
||||||
* never be called.
|
|
||||||
* @return A reference to `asyncResource`.
|
|
||||||
*/
|
|
||||||
emitDestroy(): this;
|
|
||||||
/**
|
|
||||||
* @return The unique `asyncId` assigned to the resource.
|
|
||||||
*/
|
|
||||||
asyncId(): number;
|
|
||||||
/**
|
|
||||||
* @return The same `triggerAsyncId` that is passed to the `AsyncResource` constructor.
|
|
||||||
*/
|
|
||||||
triggerAsyncId(): number;
|
|
||||||
}
|
|
||||||
interface AsyncLocalStorageOptions {
|
|
||||||
/**
|
|
||||||
* The default value to be used when no store is provided.
|
|
||||||
*/
|
|
||||||
defaultValue?: any;
|
|
||||||
/**
|
|
||||||
* A name for the `AsyncLocalStorage` value.
|
|
||||||
*/
|
|
||||||
name?: string | undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This class creates stores that stay coherent through asynchronous operations.
|
|
||||||
*
|
|
||||||
* While you can create your own implementation on top of the `node:async_hooks` module, `AsyncLocalStorage` should be preferred as it is a performant and memory
|
|
||||||
* safe implementation that involves significant optimizations that are non-obvious
|
|
||||||
* to implement.
|
|
||||||
*
|
|
||||||
* The following example uses `AsyncLocalStorage` to build a simple logger
|
|
||||||
* that assigns IDs to incoming HTTP requests and includes them in messages
|
|
||||||
* logged within each request.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import http from 'node:http';
|
|
||||||
* import { AsyncLocalStorage } from 'node:async_hooks';
|
|
||||||
*
|
|
||||||
* const asyncLocalStorage = new AsyncLocalStorage();
|
|
||||||
*
|
|
||||||
* function logWithId(msg) {
|
|
||||||
* const id = asyncLocalStorage.getStore();
|
|
||||||
* console.log(`${id !== undefined ? id : '-'}:`, msg);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* let idSeq = 0;
|
|
||||||
* http.createServer((req, res) => {
|
|
||||||
* asyncLocalStorage.run(idSeq++, () => {
|
|
||||||
* logWithId('start');
|
|
||||||
* // Imagine any chain of async operations here
|
|
||||||
* setImmediate(() => {
|
|
||||||
* logWithId('finish');
|
|
||||||
* res.end();
|
|
||||||
* });
|
|
||||||
* });
|
|
||||||
* }).listen(8080);
|
|
||||||
*
|
|
||||||
* http.get('http://localhost:8080');
|
|
||||||
* http.get('http://localhost:8080');
|
|
||||||
* // Prints:
|
|
||||||
* // 0: start
|
|
||||||
* // 0: finish
|
|
||||||
* // 1: start
|
|
||||||
* // 1: finish
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Each instance of `AsyncLocalStorage` maintains an independent storage context.
|
|
||||||
* Multiple instances can safely exist simultaneously without risk of interfering
|
|
||||||
* with each other's data.
|
|
||||||
* @since v13.10.0, v12.17.0
|
|
||||||
*/
|
|
||||||
class AsyncLocalStorage<T> {
|
|
||||||
/**
|
|
||||||
* Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
|
|
||||||
* `run()` call or after an `enterWith()` call.
|
|
||||||
*/
|
|
||||||
constructor(options?: AsyncLocalStorageOptions);
|
|
||||||
/**
|
|
||||||
* Binds the given function to the current execution context.
|
|
||||||
* @since v19.8.0
|
|
||||||
* @param fn The function to bind to the current execution context.
|
|
||||||
* @return A new function that calls `fn` within the captured execution context.
|
|
||||||
*/
|
|
||||||
static bind<Func extends (...args: any[]) => any>(fn: Func): Func;
|
|
||||||
/**
|
|
||||||
* Captures the current execution context and returns a function that accepts a
|
|
||||||
* function as an argument. Whenever the returned function is called, it
|
|
||||||
* calls the function passed to it within the captured context.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const asyncLocalStorage = new AsyncLocalStorage();
|
|
||||||
* const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot());
|
|
||||||
* const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore()));
|
|
||||||
* console.log(result); // returns 123
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* AsyncLocalStorage.snapshot() can replace the use of AsyncResource for simple
|
|
||||||
* async context tracking purposes, for example:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* class Foo {
|
|
||||||
* #runInAsyncScope = AsyncLocalStorage.snapshot();
|
|
||||||
*
|
|
||||||
* get() { return this.#runInAsyncScope(() => asyncLocalStorage.getStore()); }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* const foo = asyncLocalStorage.run(123, () => new Foo());
|
|
||||||
* console.log(asyncLocalStorage.run(321, () => foo.get())); // returns 123
|
|
||||||
* ```
|
|
||||||
* @since v19.8.0
|
|
||||||
* @return A new function with the signature `(fn: (...args) : R, ...args) : R`.
|
|
||||||
*/
|
|
||||||
static snapshot(): <R, TArgs extends any[]>(fn: (...args: TArgs) => R, ...args: TArgs) => R;
|
|
||||||
/**
|
|
||||||
* Disables the instance of `AsyncLocalStorage`. All subsequent calls
|
|
||||||
* to `asyncLocalStorage.getStore()` will return `undefined` until `asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()` is called again.
|
|
||||||
*
|
|
||||||
* When calling `asyncLocalStorage.disable()`, all current contexts linked to the
|
|
||||||
* instance will be exited.
|
|
||||||
*
|
|
||||||
* Calling `asyncLocalStorage.disable()` is required before the `asyncLocalStorage` can be garbage collected. This does not apply to stores
|
|
||||||
* provided by the `asyncLocalStorage`, as those objects are garbage collected
|
|
||||||
* along with the corresponding async resources.
|
|
||||||
*
|
|
||||||
* Use this method when the `asyncLocalStorage` is not in use anymore
|
|
||||||
* in the current process.
|
|
||||||
* @since v13.10.0, v12.17.0
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
disable(): void;
|
|
||||||
/**
|
|
||||||
* Returns the current store.
|
|
||||||
* If called outside of an asynchronous context initialized by
|
|
||||||
* calling `asyncLocalStorage.run()` or `asyncLocalStorage.enterWith()`, it
|
|
||||||
* returns `undefined`.
|
|
||||||
* @since v13.10.0, v12.17.0
|
|
||||||
*/
|
|
||||||
getStore(): T | undefined;
|
|
||||||
/**
|
|
||||||
* The name of the `AsyncLocalStorage` instance if provided.
|
|
||||||
* @since v24.0.0
|
|
||||||
*/
|
|
||||||
readonly name: string;
|
|
||||||
/**
|
|
||||||
* Runs a function synchronously within a context and returns its
|
|
||||||
* return value. The store is not accessible outside of the callback function.
|
|
||||||
* The store is accessible to any asynchronous operations created within the
|
|
||||||
* callback.
|
|
||||||
*
|
|
||||||
* The optional `args` are passed to the callback function.
|
|
||||||
*
|
|
||||||
* If the callback function throws an error, the error is thrown by `run()` too.
|
|
||||||
* The stacktrace is not impacted by this call and the context is exited.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const store = { id: 2 };
|
|
||||||
* try {
|
|
||||||
* asyncLocalStorage.run(store, () => {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the store object
|
|
||||||
* setTimeout(() => {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the store object
|
|
||||||
* }, 200);
|
|
||||||
* throw new Error();
|
|
||||||
* });
|
|
||||||
* } catch (e) {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns undefined
|
|
||||||
* // The error will be caught here
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v13.10.0, v12.17.0
|
|
||||||
*/
|
|
||||||
run<R>(store: T, callback: () => R): R;
|
|
||||||
run<R, TArgs extends any[]>(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R;
|
|
||||||
/**
|
|
||||||
* Runs a function synchronously outside of a context and returns its
|
|
||||||
* return value. The store is not accessible within the callback function or
|
|
||||||
* the asynchronous operations created within the callback. Any `getStore()` call done within the callback function will always return `undefined`.
|
|
||||||
*
|
|
||||||
* The optional `args` are passed to the callback function.
|
|
||||||
*
|
|
||||||
* If the callback function throws an error, the error is thrown by `exit()` too.
|
|
||||||
* The stacktrace is not impacted by this call and the context is re-entered.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // Within a call to run
|
|
||||||
* try {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the store object or value
|
|
||||||
* asyncLocalStorage.exit(() => {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns undefined
|
|
||||||
* throw new Error();
|
|
||||||
* });
|
|
||||||
* } catch (e) {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the same object or value
|
|
||||||
* // The error will be caught here
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v13.10.0, v12.17.0
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
exit<R, TArgs extends any[]>(callback: (...args: TArgs) => R, ...args: TArgs): R;
|
|
||||||
/**
|
|
||||||
* Transitions into the context for the remainder of the current
|
|
||||||
* synchronous execution and then persists the store through any following
|
|
||||||
* asynchronous calls.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const store = { id: 1 };
|
|
||||||
* // Replaces previous store with the given store object
|
|
||||||
* asyncLocalStorage.enterWith(store);
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the store object
|
|
||||||
* someAsyncOperation(() => {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the same object
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* This transition will continue for the _entire_ synchronous execution.
|
|
||||||
* This means that if, for example, the context is entered within an event
|
|
||||||
* handler subsequent event handlers will also run within that context unless
|
|
||||||
* specifically bound to another context with an `AsyncResource`. That is why `run()` should be preferred over `enterWith()` unless there are strong reasons
|
|
||||||
* to use the latter method.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const store = { id: 1 };
|
|
||||||
*
|
|
||||||
* emitter.on('my-event', () => {
|
|
||||||
* asyncLocalStorage.enterWith(store);
|
|
||||||
* });
|
|
||||||
* emitter.on('my-event', () => {
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the same object
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* asyncLocalStorage.getStore(); // Returns undefined
|
|
||||||
* emitter.emit('my-event');
|
|
||||||
* asyncLocalStorage.getStore(); // Returns the same object
|
|
||||||
* ```
|
|
||||||
* @since v13.11.0, v12.17.0
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
enterWith(store: T): void;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @since v17.2.0, v16.14.0
|
|
||||||
* @return A map of provider types to the corresponding numeric id.
|
|
||||||
* This map contains all the event types that might be emitted by the `async_hooks.init()` event.
|
|
||||||
*/
|
|
||||||
namespace asyncWrapProviders {
|
|
||||||
const NONE: number;
|
|
||||||
const DIRHANDLE: number;
|
|
||||||
const DNSCHANNEL: number;
|
|
||||||
const ELDHISTOGRAM: number;
|
|
||||||
const FILEHANDLE: number;
|
|
||||||
const FILEHANDLECLOSEREQ: number;
|
|
||||||
const FIXEDSIZEBLOBCOPY: number;
|
|
||||||
const FSEVENTWRAP: number;
|
|
||||||
const FSREQCALLBACK: number;
|
|
||||||
const FSREQPROMISE: number;
|
|
||||||
const GETADDRINFOREQWRAP: number;
|
|
||||||
const GETNAMEINFOREQWRAP: number;
|
|
||||||
const HEAPSNAPSHOT: number;
|
|
||||||
const HTTP2SESSION: number;
|
|
||||||
const HTTP2STREAM: number;
|
|
||||||
const HTTP2PING: number;
|
|
||||||
const HTTP2SETTINGS: number;
|
|
||||||
const HTTPINCOMINGMESSAGE: number;
|
|
||||||
const HTTPCLIENTREQUEST: number;
|
|
||||||
const JSSTREAM: number;
|
|
||||||
const JSUDPWRAP: number;
|
|
||||||
const MESSAGEPORT: number;
|
|
||||||
const PIPECONNECTWRAP: number;
|
|
||||||
const PIPESERVERWRAP: number;
|
|
||||||
const PIPEWRAP: number;
|
|
||||||
const PROCESSWRAP: number;
|
|
||||||
const PROMISE: number;
|
|
||||||
const QUERYWRAP: number;
|
|
||||||
const SHUTDOWNWRAP: number;
|
|
||||||
const SIGNALWRAP: number;
|
|
||||||
const STATWATCHER: number;
|
|
||||||
const STREAMPIPE: number;
|
|
||||||
const TCPCONNECTWRAP: number;
|
|
||||||
const TCPSERVERWRAP: number;
|
|
||||||
const TCPWRAP: number;
|
|
||||||
const TTYWRAP: number;
|
|
||||||
const UDPSENDWRAP: number;
|
|
||||||
const UDPWRAP: number;
|
|
||||||
const SIGINTWATCHDOG: number;
|
|
||||||
const WORKER: number;
|
|
||||||
const WORKERHEAPSNAPSHOT: number;
|
|
||||||
const WRITEWRAP: number;
|
|
||||||
const ZLIB: number;
|
|
||||||
const CHECKPRIMEREQUEST: number;
|
|
||||||
const PBKDF2REQUEST: number;
|
|
||||||
const KEYPAIRGENREQUEST: number;
|
|
||||||
const KEYGENREQUEST: number;
|
|
||||||
const KEYEXPORTREQUEST: number;
|
|
||||||
const CIPHERREQUEST: number;
|
|
||||||
const DERIVEBITSREQUEST: number;
|
|
||||||
const HASHREQUEST: number;
|
|
||||||
const RANDOMBYTESREQUEST: number;
|
|
||||||
const RANDOMPRIMEREQUEST: number;
|
|
||||||
const SCRYPTREQUEST: number;
|
|
||||||
const SIGNREQUEST: number;
|
|
||||||
const TLSWRAP: number;
|
|
||||||
const VERIFYREQUEST: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
declare module "node:async_hooks" {
|
|
||||||
export * from "async_hooks";
|
|
||||||
}
|
|
||||||
463
node_modules/@types/node/buffer.buffer.d.ts
generated
vendored
463
node_modules/@types/node/buffer.buffer.d.ts
generated
vendored
@@ -1,463 +0,0 @@
|
|||||||
declare module "buffer" {
|
|
||||||
type ImplicitArrayBuffer<T extends WithImplicitCoercion<ArrayBufferLike>> = T extends
|
|
||||||
{ valueOf(): infer V extends ArrayBufferLike } ? V : T;
|
|
||||||
global {
|
|
||||||
interface BufferConstructor {
|
|
||||||
// see buffer.d.ts for implementation shared with all TypeScript versions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocates a new buffer containing the given {str}.
|
|
||||||
*
|
|
||||||
* @param str String to store in buffer.
|
|
||||||
* @param encoding encoding to use, optional. Default is 'utf8'
|
|
||||||
* @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead.
|
|
||||||
*/
|
|
||||||
new(str: string, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new buffer of {size} octets.
|
|
||||||
*
|
|
||||||
* @param size count of octets to allocate.
|
|
||||||
* @deprecated since v10.0.0 - Use `Buffer.alloc()` instead (also see `Buffer.allocUnsafe()`).
|
|
||||||
*/
|
|
||||||
new(size: number): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new buffer containing the given {array} of octets.
|
|
||||||
*
|
|
||||||
* @param array The octets to store.
|
|
||||||
* @deprecated since v10.0.0 - Use `Buffer.from(array)` instead.
|
|
||||||
*/
|
|
||||||
new(array: ArrayLike<number>): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Produces a Buffer backed by the same allocated memory as
|
|
||||||
* the given {ArrayBuffer}/{SharedArrayBuffer}.
|
|
||||||
*
|
|
||||||
* @param arrayBuffer The ArrayBuffer with which to share memory.
|
|
||||||
* @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead.
|
|
||||||
*/
|
|
||||||
new<TArrayBuffer extends ArrayBufferLike = ArrayBuffer>(arrayBuffer: TArrayBuffer): Buffer<TArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new `Buffer` using an `array` of bytes in the range `0` – `255`.
|
|
||||||
* Array entries outside that range will be truncated to fit into it.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'.
|
|
||||||
* const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* If `array` is an `Array`-like object (that is, one with a `length` property of
|
|
||||||
* type `number`), it is treated as if it is an array, unless it is a `Buffer` or
|
|
||||||
* a `Uint8Array`. This means all other `TypedArray` variants get treated as an
|
|
||||||
* `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use
|
|
||||||
* `Buffer.copyBytesFrom()`.
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `array` is not an `Array` or another type
|
|
||||||
* appropriate for `Buffer.from()` variants.
|
|
||||||
*
|
|
||||||
* `Buffer.from(array)` and `Buffer.from(string)` may also use the internal
|
|
||||||
* `Buffer` pool like `Buffer.allocUnsafe()` does.
|
|
||||||
* @since v5.10.0
|
|
||||||
*/
|
|
||||||
from(array: WithImplicitCoercion<ArrayLike<number>>): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* This creates a view of the `ArrayBuffer` without copying the underlying
|
|
||||||
* memory. For example, when passed a reference to the `.buffer` property of a
|
|
||||||
* `TypedArray` instance, the newly created `Buffer` will share the same
|
|
||||||
* allocated memory as the `TypedArray`'s underlying `ArrayBuffer`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const arr = new Uint16Array(2);
|
|
||||||
*
|
|
||||||
* arr[0] = 5000;
|
|
||||||
* arr[1] = 4000;
|
|
||||||
*
|
|
||||||
* // Shares memory with `arr`.
|
|
||||||
* const buf = Buffer.from(arr.buffer);
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 88 13 a0 0f>
|
|
||||||
*
|
|
||||||
* // Changing the original Uint16Array changes the Buffer also.
|
|
||||||
* arr[1] = 6000;
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 88 13 70 17>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The optional `byteOffset` and `length` arguments specify a memory range within
|
|
||||||
* the `arrayBuffer` that will be shared by the `Buffer`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const ab = new ArrayBuffer(10);
|
|
||||||
* const buf = Buffer.from(ab, 0, 2);
|
|
||||||
*
|
|
||||||
* console.log(buf.length);
|
|
||||||
* // Prints: 2
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer` or a
|
|
||||||
* `SharedArrayBuffer` or another type appropriate for `Buffer.from()`
|
|
||||||
* variants.
|
|
||||||
*
|
|
||||||
* It is important to remember that a backing `ArrayBuffer` can cover a range
|
|
||||||
* of memory that extends beyond the bounds of a `TypedArray` view. A new
|
|
||||||
* `Buffer` created using the `buffer` property of a `TypedArray` may extend
|
|
||||||
* beyond the range of the `TypedArray`:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const arrA = Uint8Array.from([0x63, 0x64, 0x65, 0x66]); // 4 elements
|
|
||||||
* const arrB = new Uint8Array(arrA.buffer, 1, 2); // 2 elements
|
|
||||||
* console.log(arrA.buffer === arrB.buffer); // true
|
|
||||||
*
|
|
||||||
* const buf = Buffer.from(arrB.buffer);
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 63 64 65 66>
|
|
||||||
* ```
|
|
||||||
* @since v5.10.0
|
|
||||||
* @param arrayBuffer An `ArrayBuffer`, `SharedArrayBuffer`, for example the
|
|
||||||
* `.buffer` property of a `TypedArray`.
|
|
||||||
* @param byteOffset Index of first byte to expose. **Default:** `0`.
|
|
||||||
* @param length Number of bytes to expose. **Default:**
|
|
||||||
* `arrayBuffer.byteLength - byteOffset`.
|
|
||||||
*/
|
|
||||||
from<TArrayBuffer extends WithImplicitCoercion<ArrayBufferLike>>(
|
|
||||||
arrayBuffer: TArrayBuffer,
|
|
||||||
byteOffset?: number,
|
|
||||||
length?: number,
|
|
||||||
): Buffer<ImplicitArrayBuffer<TArrayBuffer>>;
|
|
||||||
/**
|
|
||||||
* Creates a new `Buffer` containing `string`. The `encoding` parameter identifies
|
|
||||||
* the character encoding to be used when converting `string` into bytes.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf1 = Buffer.from('this is a tést');
|
|
||||||
* const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex');
|
|
||||||
*
|
|
||||||
* console.log(buf1.toString());
|
|
||||||
* // Prints: this is a tést
|
|
||||||
* console.log(buf2.toString());
|
|
||||||
* // Prints: this is a tést
|
|
||||||
* console.log(buf1.toString('latin1'));
|
|
||||||
* // Prints: this is a tést
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `string` is not a string or another type
|
|
||||||
* appropriate for `Buffer.from()` variants.
|
|
||||||
*
|
|
||||||
* `Buffer.from(string)` may also use the internal `Buffer` pool like
|
|
||||||
* `Buffer.allocUnsafe()` does.
|
|
||||||
* @since v5.10.0
|
|
||||||
* @param string A string to encode.
|
|
||||||
* @param encoding The encoding of `string`. **Default:** `'utf8'`.
|
|
||||||
*/
|
|
||||||
from(string: WithImplicitCoercion<string>, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
|
|
||||||
from(arrayOrString: WithImplicitCoercion<ArrayLike<number> | string>): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Creates a new Buffer using the passed {data}
|
|
||||||
* @param values to create a new Buffer
|
|
||||||
*/
|
|
||||||
of(...items: number[]): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Returns a new `Buffer` which is the result of concatenating all the `Buffer` instances in the `list` together.
|
|
||||||
*
|
|
||||||
* If the list has no items, or if the `totalLength` is 0, then a new zero-length `Buffer` is returned.
|
|
||||||
*
|
|
||||||
* If `totalLength` is not provided, it is calculated from the `Buffer` instances
|
|
||||||
* in `list` by adding their lengths.
|
|
||||||
*
|
|
||||||
* If `totalLength` is provided, it is coerced to an unsigned integer. If the
|
|
||||||
* combined length of the `Buffer`s in `list` exceeds `totalLength`, the result is
|
|
||||||
* truncated to `totalLength`. If the combined length of the `Buffer`s in `list` is
|
|
||||||
* less than `totalLength`, the remaining space is filled with zeros.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* // Create a single `Buffer` from a list of three `Buffer` instances.
|
|
||||||
*
|
|
||||||
* const buf1 = Buffer.alloc(10);
|
|
||||||
* const buf2 = Buffer.alloc(14);
|
|
||||||
* const buf3 = Buffer.alloc(18);
|
|
||||||
* const totalLength = buf1.length + buf2.length + buf3.length;
|
|
||||||
*
|
|
||||||
* console.log(totalLength);
|
|
||||||
* // Prints: 42
|
|
||||||
*
|
|
||||||
* const bufA = Buffer.concat([buf1, buf2, buf3], totalLength);
|
|
||||||
*
|
|
||||||
* console.log(bufA);
|
|
||||||
* // Prints: <Buffer 00 00 00 00 ...>
|
|
||||||
* console.log(bufA.length);
|
|
||||||
* // Prints: 42
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* `Buffer.concat()` may also use the internal `Buffer` pool like `Buffer.allocUnsafe()` does.
|
|
||||||
* @since v0.7.11
|
|
||||||
* @param list List of `Buffer` or {@link Uint8Array} instances to concatenate.
|
|
||||||
* @param totalLength Total length of the `Buffer` instances in `list` when concatenated.
|
|
||||||
*/
|
|
||||||
concat(list: readonly Uint8Array[], totalLength?: number): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Copies the underlying memory of `view` into a new `Buffer`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const u16 = new Uint16Array([0, 0xffff]);
|
|
||||||
* const buf = Buffer.copyBytesFrom(u16, 1, 1);
|
|
||||||
* u16[1] = 0;
|
|
||||||
* console.log(buf.length); // 2
|
|
||||||
* console.log(buf[0]); // 255
|
|
||||||
* console.log(buf[1]); // 255
|
|
||||||
* ```
|
|
||||||
* @since v19.8.0
|
|
||||||
* @param view The {TypedArray} to copy.
|
|
||||||
* @param [offset=0] The starting offset within `view`.
|
|
||||||
* @param [length=view.length - offset] The number of elements from `view` to copy.
|
|
||||||
*/
|
|
||||||
copyBytesFrom(view: NodeJS.TypedArray, offset?: number, length?: number): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.alloc(5);
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 00 00 00 00 00>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown.
|
|
||||||
*
|
|
||||||
* If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.alloc(5, 'a');
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 61 61 61 61 61>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* If both `fill` and `encoding` are specified, the allocated `Buffer` will be
|
|
||||||
* initialized by calling `buf.fill(fill, encoding)`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance
|
|
||||||
* contents will never contain sensitive data from previous allocations, including
|
|
||||||
* data that might not have been allocated for `Buffer`s.
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `size` is not a number.
|
|
||||||
* @since v5.10.0
|
|
||||||
* @param size The desired length of the new `Buffer`.
|
|
||||||
* @param [fill=0] A value to pre-fill the new `Buffer` with.
|
|
||||||
* @param [encoding='utf8'] If `fill` is a string, this is its encoding.
|
|
||||||
*/
|
|
||||||
alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown.
|
|
||||||
*
|
|
||||||
* The underlying memory for `Buffer` instances created in this way is _not_
|
|
||||||
* _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.allocUnsafe(10);
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints (contents may vary): <Buffer a0 8b 28 3f 01 00 00 00 50 32>
|
|
||||||
*
|
|
||||||
* buf.fill(0);
|
|
||||||
*
|
|
||||||
* console.log(buf);
|
|
||||||
* // Prints: <Buffer 00 00 00 00 00 00 00 00 00 00>
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `size` is not a number.
|
|
||||||
*
|
|
||||||
* The `Buffer` module pre-allocates an internal `Buffer` instance of
|
|
||||||
* size `Buffer.poolSize` that is used as a pool for the fast allocation of new `Buffer` instances created using `Buffer.allocUnsafe()`, `Buffer.from(array)`,
|
|
||||||
* and `Buffer.concat()` only when `size` is less than `Buffer.poolSize >>> 1` (floor of `Buffer.poolSize` divided by two).
|
|
||||||
*
|
|
||||||
* Use of this pre-allocated internal memory pool is a key difference between
|
|
||||||
* calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`.
|
|
||||||
* Specifically, `Buffer.alloc(size, fill)` will _never_ use the internal `Buffer`pool, while `Buffer.allocUnsafe(size).fill(fill)`_will_ use the internal`Buffer` pool if `size` is less
|
|
||||||
* than or equal to half `Buffer.poolSize`. The
|
|
||||||
* difference is subtle but can be important when an application requires the
|
|
||||||
* additional performance that `Buffer.allocUnsafe()` provides.
|
|
||||||
* @since v5.10.0
|
|
||||||
* @param size The desired length of the new `Buffer`.
|
|
||||||
*/
|
|
||||||
allocUnsafe(size: number): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. A zero-length `Buffer` is created if
|
|
||||||
* `size` is 0.
|
|
||||||
*
|
|
||||||
* The underlying memory for `Buffer` instances created in this way is _not_
|
|
||||||
* _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `buf.fill(0)` to initialize
|
|
||||||
* such `Buffer` instances with zeroes.
|
|
||||||
*
|
|
||||||
* When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances,
|
|
||||||
* allocations under 4 KiB are sliced from a single pre-allocated `Buffer`. This
|
|
||||||
* allows applications to avoid the garbage collection overhead of creating many
|
|
||||||
* individually allocated `Buffer` instances. This approach improves both
|
|
||||||
* performance and memory usage by eliminating the need to track and clean up as
|
|
||||||
* many individual `ArrayBuffer` objects.
|
|
||||||
*
|
|
||||||
* However, in the case where a developer may need to retain a small chunk of
|
|
||||||
* memory from a pool for an indeterminate amount of time, it may be appropriate
|
|
||||||
* to create an un-pooled `Buffer` instance using `Buffer.allocUnsafeSlow()` and
|
|
||||||
* then copying out the relevant bits.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* // Need to keep around a few small chunks of memory.
|
|
||||||
* const store = [];
|
|
||||||
*
|
|
||||||
* socket.on('readable', () => {
|
|
||||||
* let data;
|
|
||||||
* while (null !== (data = readable.read())) {
|
|
||||||
* // Allocate for retained data.
|
|
||||||
* const sb = Buffer.allocUnsafeSlow(10);
|
|
||||||
*
|
|
||||||
* // Copy the data into the new allocation.
|
|
||||||
* data.copy(sb, 0, 0, 10);
|
|
||||||
*
|
|
||||||
* store.push(sb);
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* A `TypeError` will be thrown if `size` is not a number.
|
|
||||||
* @since v5.12.0
|
|
||||||
* @param size The desired length of the new `Buffer`.
|
|
||||||
*/
|
|
||||||
allocUnsafeSlow(size: number): Buffer<ArrayBuffer>;
|
|
||||||
}
|
|
||||||
interface Buffer<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> extends Uint8Array<TArrayBuffer> {
|
|
||||||
// see buffer.d.ts for implementation shared with all TypeScript versions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new `Buffer` that references the same memory as the original, but
|
|
||||||
* offset and cropped by the `start` and `end` indices.
|
|
||||||
*
|
|
||||||
* This method is not compatible with the `Uint8Array.prototype.slice()`,
|
|
||||||
* which is a superclass of `Buffer`. To copy the slice, use`Uint8Array.prototype.slice()`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.from('buffer');
|
|
||||||
*
|
|
||||||
* const copiedBuf = Uint8Array.prototype.slice.call(buf);
|
|
||||||
* copiedBuf[0]++;
|
|
||||||
* console.log(copiedBuf.toString());
|
|
||||||
* // Prints: cuffer
|
|
||||||
*
|
|
||||||
* console.log(buf.toString());
|
|
||||||
* // Prints: buffer
|
|
||||||
*
|
|
||||||
* // With buf.slice(), the original buffer is modified.
|
|
||||||
* const notReallyCopiedBuf = buf.slice();
|
|
||||||
* notReallyCopiedBuf[0]++;
|
|
||||||
* console.log(notReallyCopiedBuf.toString());
|
|
||||||
* // Prints: cuffer
|
|
||||||
* console.log(buf.toString());
|
|
||||||
* // Also prints: cuffer (!)
|
|
||||||
* ```
|
|
||||||
* @since v0.3.0
|
|
||||||
* @deprecated Use `subarray` instead.
|
|
||||||
* @param [start=0] Where the new `Buffer` will start.
|
|
||||||
* @param [end=buf.length] Where the new `Buffer` will end (not inclusive).
|
|
||||||
*/
|
|
||||||
slice(start?: number, end?: number): Buffer<ArrayBuffer>;
|
|
||||||
/**
|
|
||||||
* Returns a new `Buffer` that references the same memory as the original, but
|
|
||||||
* offset and cropped by the `start` and `end` indices.
|
|
||||||
*
|
|
||||||
* Specifying `end` greater than `buf.length` will return the same result as
|
|
||||||
* that of `end` equal to `buf.length`.
|
|
||||||
*
|
|
||||||
* This method is inherited from [`TypedArray.prototype.subarray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray).
|
|
||||||
*
|
|
||||||
* Modifying the new `Buffer` slice will modify the memory in the original `Buffer`because the allocated memory of the two objects overlap.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* // Create a `Buffer` with the ASCII alphabet, take a slice, and modify one byte
|
|
||||||
* // from the original `Buffer`.
|
|
||||||
*
|
|
||||||
* const buf1 = Buffer.allocUnsafe(26);
|
|
||||||
*
|
|
||||||
* for (let i = 0; i < 26; i++) {
|
|
||||||
* // 97 is the decimal ASCII value for 'a'.
|
|
||||||
* buf1[i] = i + 97;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* const buf2 = buf1.subarray(0, 3);
|
|
||||||
*
|
|
||||||
* console.log(buf2.toString('ascii', 0, buf2.length));
|
|
||||||
* // Prints: abc
|
|
||||||
*
|
|
||||||
* buf1[0] = 33;
|
|
||||||
*
|
|
||||||
* console.log(buf2.toString('ascii', 0, buf2.length));
|
|
||||||
* // Prints: !bc
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Specifying negative indexes causes the slice to be generated relative to the
|
|
||||||
* end of `buf` rather than the beginning.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import { Buffer } from 'node:buffer';
|
|
||||||
*
|
|
||||||
* const buf = Buffer.from('buffer');
|
|
||||||
*
|
|
||||||
* console.log(buf.subarray(-6, -1).toString());
|
|
||||||
* // Prints: buffe
|
|
||||||
* // (Equivalent to buf.subarray(0, 5).)
|
|
||||||
*
|
|
||||||
* console.log(buf.subarray(-6, -2).toString());
|
|
||||||
* // Prints: buff
|
|
||||||
* // (Equivalent to buf.subarray(0, 4).)
|
|
||||||
*
|
|
||||||
* console.log(buf.subarray(-5, -2).toString());
|
|
||||||
* // Prints: uff
|
|
||||||
* // (Equivalent to buf.subarray(1, 4).)
|
|
||||||
* ```
|
|
||||||
* @since v3.0.0
|
|
||||||
* @param [start=0] Where the new `Buffer` will start.
|
|
||||||
* @param [end=buf.length] Where the new `Buffer` will end (not inclusive).
|
|
||||||
*/
|
|
||||||
subarray(start?: number, end?: number): Buffer<TArrayBuffer>;
|
|
||||||
}
|
|
||||||
type NonSharedBuffer = Buffer<ArrayBuffer>;
|
|
||||||
type AllowSharedBuffer = Buffer<ArrayBufferLike>;
|
|
||||||
}
|
|
||||||
/** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */
|
|
||||||
var SlowBuffer: {
|
|
||||||
/** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */
|
|
||||||
new(size: number): Buffer<ArrayBuffer>;
|
|
||||||
prototype: Buffer;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
1930
node_modules/@types/node/buffer.d.ts
generated
vendored
1930
node_modules/@types/node/buffer.d.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
1453
node_modules/@types/node/child_process.d.ts
generated
vendored
1453
node_modules/@types/node/child_process.d.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
579
node_modules/@types/node/cluster.d.ts
generated
vendored
579
node_modules/@types/node/cluster.d.ts
generated
vendored
@@ -1,579 +0,0 @@
|
|||||||
/**
|
|
||||||
* Clusters of Node.js processes can be used to run multiple instances of Node.js
|
|
||||||
* that can distribute workloads among their application threads. When process isolation
|
|
||||||
* is not needed, use the [`worker_threads`](https://nodejs.org/docs/latest-v24.x/api/worker_threads.html)
|
|
||||||
* module instead, which allows running multiple application threads within a single Node.js instance.
|
|
||||||
*
|
|
||||||
* The cluster module allows easy creation of child processes that all share
|
|
||||||
* server ports.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import cluster from 'node:cluster';
|
|
||||||
* import http from 'node:http';
|
|
||||||
* import { availableParallelism } from 'node:os';
|
|
||||||
* import process from 'node:process';
|
|
||||||
*
|
|
||||||
* const numCPUs = availableParallelism();
|
|
||||||
*
|
|
||||||
* if (cluster.isPrimary) {
|
|
||||||
* console.log(`Primary ${process.pid} is running`);
|
|
||||||
*
|
|
||||||
* // Fork workers.
|
|
||||||
* for (let i = 0; i < numCPUs; i++) {
|
|
||||||
* cluster.fork();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* cluster.on('exit', (worker, code, signal) => {
|
|
||||||
* console.log(`worker ${worker.process.pid} died`);
|
|
||||||
* });
|
|
||||||
* } else {
|
|
||||||
* // Workers can share any TCP connection
|
|
||||||
* // In this case it is an HTTP server
|
|
||||||
* http.createServer((req, res) => {
|
|
||||||
* res.writeHead(200);
|
|
||||||
* res.end('hello world\n');
|
|
||||||
* }).listen(8000);
|
|
||||||
*
|
|
||||||
* console.log(`Worker ${process.pid} started`);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Running Node.js will now share port 8000 between the workers:
|
|
||||||
*
|
|
||||||
* ```console
|
|
||||||
* $ node server.js
|
|
||||||
* Primary 3596 is running
|
|
||||||
* Worker 4324 started
|
|
||||||
* Worker 4520 started
|
|
||||||
* Worker 6056 started
|
|
||||||
* Worker 5644 started
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* On Windows, it is not yet possible to set up a named pipe server in a worker.
|
|
||||||
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/cluster.js)
|
|
||||||
*/
|
|
||||||
declare module "cluster" {
|
|
||||||
import * as child from "node:child_process";
|
|
||||||
import EventEmitter = require("node:events");
|
|
||||||
import * as net from "node:net";
|
|
||||||
type SerializationType = "json" | "advanced";
|
|
||||||
export interface ClusterSettings {
|
|
||||||
/**
|
|
||||||
* List of string arguments passed to the Node.js executable.
|
|
||||||
* @default process.execArgv
|
|
||||||
*/
|
|
||||||
execArgv?: string[] | undefined;
|
|
||||||
/**
|
|
||||||
* File path to worker file.
|
|
||||||
* @default process.argv[1]
|
|
||||||
*/
|
|
||||||
exec?: string | undefined;
|
|
||||||
/**
|
|
||||||
* String arguments passed to worker.
|
|
||||||
* @default process.argv.slice(2)
|
|
||||||
*/
|
|
||||||
args?: string[] | undefined;
|
|
||||||
/**
|
|
||||||
* Whether or not to send output to parent's stdio.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
silent?: boolean | undefined;
|
|
||||||
/**
|
|
||||||
* Configures the stdio of forked processes. Because the cluster module relies on IPC to function, this configuration must
|
|
||||||
* contain an `'ipc'` entry. When this option is provided, it overrides `silent`. See [`child_prcess.spawn()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processspawncommand-args-options)'s
|
|
||||||
* [`stdio`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#optionsstdio).
|
|
||||||
*/
|
|
||||||
stdio?: any[] | undefined;
|
|
||||||
/**
|
|
||||||
* Sets the user identity of the process. (See [`setuid(2)`](https://man7.org/linux/man-pages/man2/setuid.2.html).)
|
|
||||||
*/
|
|
||||||
uid?: number | undefined;
|
|
||||||
/**
|
|
||||||
* Sets the group identity of the process. (See [`setgid(2)`](https://man7.org/linux/man-pages/man2/setgid.2.html).)
|
|
||||||
*/
|
|
||||||
gid?: number | undefined;
|
|
||||||
/**
|
|
||||||
* Sets inspector port of worker. This can be a number, or a function that takes no arguments and returns a number.
|
|
||||||
* By default each worker gets its own port, incremented from the primary's `process.debugPort`.
|
|
||||||
*/
|
|
||||||
inspectPort?: number | (() => number) | undefined;
|
|
||||||
/**
|
|
||||||
* Specify the kind of serialization used for sending messages between processes. Possible values are `'json'` and `'advanced'`.
|
|
||||||
* See [Advanced serialization for `child_process`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#advanced-serialization) for more details.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
serialization?: SerializationType | undefined;
|
|
||||||
/**
|
|
||||||
* Current working directory of the worker process.
|
|
||||||
* @default undefined (inherits from parent process)
|
|
||||||
*/
|
|
||||||
cwd?: string | undefined;
|
|
||||||
/**
|
|
||||||
* Hide the forked processes console window that would normally be created on Windows systems.
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
windowsHide?: boolean | undefined;
|
|
||||||
}
|
|
||||||
export interface Address {
|
|
||||||
address: string;
|
|
||||||
port: number;
|
|
||||||
/**
|
|
||||||
* The `addressType` is one of:
|
|
||||||
*
|
|
||||||
* * `4` (TCPv4)
|
|
||||||
* * `6` (TCPv6)
|
|
||||||
* * `-1` (Unix domain socket)
|
|
||||||
* * `'udp4'` or `'udp6'` (UDPv4 or UDPv6)
|
|
||||||
*/
|
|
||||||
addressType: 4 | 6 | -1 | "udp4" | "udp6";
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A `Worker` object contains all public information and method about a worker.
|
|
||||||
* In the primary it can be obtained using `cluster.workers`. In a worker
|
|
||||||
* it can be obtained using `cluster.worker`.
|
|
||||||
* @since v0.7.0
|
|
||||||
*/
|
|
||||||
export class Worker extends EventEmitter {
|
|
||||||
/**
|
|
||||||
* Each new worker is given its own unique id, this id is stored in the `id`.
|
|
||||||
*
|
|
||||||
* While a worker is alive, this is the key that indexes it in `cluster.workers`.
|
|
||||||
* @since v0.8.0
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
/**
|
|
||||||
* All workers are created using [`child_process.fork()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processforkmodulepath-args-options), the returned object
|
|
||||||
* from this function is stored as `.process`. In a worker, the global `process` is stored.
|
|
||||||
*
|
|
||||||
* See: [Child Process module](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processforkmodulepath-args-options).
|
|
||||||
*
|
|
||||||
* Workers will call `process.exit(0)` if the `'disconnect'` event occurs
|
|
||||||
* on `process` and `.exitedAfterDisconnect` is not `true`. This protects against
|
|
||||||
* accidental disconnection.
|
|
||||||
* @since v0.7.0
|
|
||||||
*/
|
|
||||||
process: child.ChildProcess;
|
|
||||||
/**
|
|
||||||
* Send a message to a worker or primary, optionally with a handle.
|
|
||||||
*
|
|
||||||
* In the primary, this sends a message to a specific worker. It is identical to [`ChildProcess.send()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
|
|
||||||
*
|
|
||||||
* In a worker, this sends a message to the primary. It is identical to `process.send()`.
|
|
||||||
*
|
|
||||||
* This example will echo back all messages from the primary:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* if (cluster.isPrimary) {
|
|
||||||
* const worker = cluster.fork();
|
|
||||||
* worker.send('hi there');
|
|
||||||
*
|
|
||||||
* } else if (cluster.isWorker) {
|
|
||||||
* process.on('message', (msg) => {
|
|
||||||
* process.send(msg);
|
|
||||||
* });
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v0.7.0
|
|
||||||
* @param options The `options` argument, if present, is an object used to parameterize the sending of certain types of handles.
|
|
||||||
*/
|
|
||||||
send(message: child.Serializable, callback?: (error: Error | null) => void): boolean;
|
|
||||||
send(
|
|
||||||
message: child.Serializable,
|
|
||||||
sendHandle: child.SendHandle,
|
|
||||||
callback?: (error: Error | null) => void,
|
|
||||||
): boolean;
|
|
||||||
send(
|
|
||||||
message: child.Serializable,
|
|
||||||
sendHandle: child.SendHandle,
|
|
||||||
options?: child.MessageOptions,
|
|
||||||
callback?: (error: Error | null) => void,
|
|
||||||
): boolean;
|
|
||||||
/**
|
|
||||||
* This function will kill the worker. In the primary worker, it does this by
|
|
||||||
* disconnecting the `worker.process`, and once disconnected, killing with `signal`. In the worker, it does it by killing the process with `signal`.
|
|
||||||
*
|
|
||||||
* The `kill()` function kills the worker process without waiting for a graceful
|
|
||||||
* disconnect, it has the same behavior as `worker.process.kill()`.
|
|
||||||
*
|
|
||||||
* This method is aliased as `worker.destroy()` for backwards compatibility.
|
|
||||||
*
|
|
||||||
* In a worker, `process.kill()` exists, but it is not this function;
|
|
||||||
* it is [`kill()`](https://nodejs.org/docs/latest-v24.x/api/process.html#processkillpid-signal).
|
|
||||||
* @since v0.9.12
|
|
||||||
* @param [signal='SIGTERM'] Name of the kill signal to send to the worker process.
|
|
||||||
*/
|
|
||||||
kill(signal?: string): void;
|
|
||||||
destroy(signal?: string): void;
|
|
||||||
/**
|
|
||||||
* In a worker, this function will close all servers, wait for the `'close'` event
|
|
||||||
* on those servers, and then disconnect the IPC channel.
|
|
||||||
*
|
|
||||||
* In the primary, an internal message is sent to the worker causing it to call `.disconnect()` on itself.
|
|
||||||
*
|
|
||||||
* Causes `.exitedAfterDisconnect` to be set.
|
|
||||||
*
|
|
||||||
* After a server is closed, it will no longer accept new connections,
|
|
||||||
* but connections may be accepted by any other listening worker. Existing
|
|
||||||
* connections will be allowed to close as usual. When no more connections exist,
|
|
||||||
* see `server.close()`, the IPC channel to the worker will close allowing it
|
|
||||||
* to die gracefully.
|
|
||||||
*
|
|
||||||
* The above applies _only_ to server connections, client connections are not
|
|
||||||
* automatically closed by workers, and disconnect does not wait for them to close
|
|
||||||
* before exiting.
|
|
||||||
*
|
|
||||||
* In a worker, `process.disconnect` exists, but it is not this function;
|
|
||||||
* it is `disconnect()`.
|
|
||||||
*
|
|
||||||
* Because long living server connections may block workers from disconnecting, it
|
|
||||||
* may be useful to send a message, so application specific actions may be taken to
|
|
||||||
* close them. It also may be useful to implement a timeout, killing a worker if
|
|
||||||
* the `'disconnect'` event has not been emitted after some time.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import net from 'node:net';
|
|
||||||
*
|
|
||||||
* if (cluster.isPrimary) {
|
|
||||||
* const worker = cluster.fork();
|
|
||||||
* let timeout;
|
|
||||||
*
|
|
||||||
* worker.on('listening', (address) => {
|
|
||||||
* worker.send('shutdown');
|
|
||||||
* worker.disconnect();
|
|
||||||
* timeout = setTimeout(() => {
|
|
||||||
* worker.kill();
|
|
||||||
* }, 2000);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* worker.on('disconnect', () => {
|
|
||||||
* clearTimeout(timeout);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* } else if (cluster.isWorker) {
|
|
||||||
* const server = net.createServer((socket) => {
|
|
||||||
* // Connections never end
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* server.listen(8000);
|
|
||||||
*
|
|
||||||
* process.on('message', (msg) => {
|
|
||||||
* if (msg === 'shutdown') {
|
|
||||||
* // Initiate graceful close of any connections to server
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v0.7.7
|
|
||||||
* @return A reference to `worker`.
|
|
||||||
*/
|
|
||||||
disconnect(): this;
|
|
||||||
/**
|
|
||||||
* This function returns `true` if the worker is connected to its primary via its
|
|
||||||
* IPC channel, `false` otherwise. A worker is connected to its primary after it
|
|
||||||
* has been created. It is disconnected after the `'disconnect'` event is emitted.
|
|
||||||
* @since v0.11.14
|
|
||||||
*/
|
|
||||||
isConnected(): boolean;
|
|
||||||
/**
|
|
||||||
* This function returns `true` if the worker's process has terminated (either
|
|
||||||
* because of exiting or being signaled). Otherwise, it returns `false`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import cluster from 'node:cluster';
|
|
||||||
* import http from 'node:http';
|
|
||||||
* import { availableParallelism } from 'node:os';
|
|
||||||
* import process from 'node:process';
|
|
||||||
*
|
|
||||||
* const numCPUs = availableParallelism();
|
|
||||||
*
|
|
||||||
* if (cluster.isPrimary) {
|
|
||||||
* console.log(`Primary ${process.pid} is running`);
|
|
||||||
*
|
|
||||||
* // Fork workers.
|
|
||||||
* for (let i = 0; i < numCPUs; i++) {
|
|
||||||
* cluster.fork();
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* cluster.on('fork', (worker) => {
|
|
||||||
* console.log('worker is dead:', worker.isDead());
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* cluster.on('exit', (worker, code, signal) => {
|
|
||||||
* console.log('worker is dead:', worker.isDead());
|
|
||||||
* });
|
|
||||||
* } else {
|
|
||||||
* // Workers can share any TCP connection. In this case, it is an HTTP server.
|
|
||||||
* http.createServer((req, res) => {
|
|
||||||
* res.writeHead(200);
|
|
||||||
* res.end(`Current process\n ${process.pid}`);
|
|
||||||
* process.kill(process.pid);
|
|
||||||
* }).listen(8000);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v0.11.14
|
|
||||||
*/
|
|
||||||
isDead(): boolean;
|
|
||||||
/**
|
|
||||||
* This property is `true` if the worker exited due to `.disconnect()`.
|
|
||||||
* If the worker exited any other way, it is `false`. If the
|
|
||||||
* worker has not exited, it is `undefined`.
|
|
||||||
*
|
|
||||||
* The boolean `worker.exitedAfterDisconnect` allows distinguishing between
|
|
||||||
* voluntary and accidental exit, the primary may choose not to respawn a worker
|
|
||||||
* based on this value.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* cluster.on('exit', (worker, code, signal) => {
|
|
||||||
* if (worker.exitedAfterDisconnect === true) {
|
|
||||||
* console.log('Oh, it was just voluntary – no need to worry');
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // kill worker
|
|
||||||
* worker.kill();
|
|
||||||
* ```
|
|
||||||
* @since v6.0.0
|
|
||||||
*/
|
|
||||||
exitedAfterDisconnect: boolean;
|
|
||||||
/**
|
|
||||||
* events.EventEmitter
|
|
||||||
* 1. disconnect
|
|
||||||
* 2. error
|
|
||||||
* 3. exit
|
|
||||||
* 4. listening
|
|
||||||
* 5. message
|
|
||||||
* 6. online
|
|
||||||
*/
|
|
||||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
addListener(event: "disconnect", listener: () => void): this;
|
|
||||||
addListener(event: "error", listener: (error: Error) => void): this;
|
|
||||||
addListener(event: "exit", listener: (code: number, signal: string) => void): this;
|
|
||||||
addListener(event: "listening", listener: (address: Address) => void): this;
|
|
||||||
addListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
addListener(event: "online", listener: () => void): this;
|
|
||||||
emit(event: string | symbol, ...args: any[]): boolean;
|
|
||||||
emit(event: "disconnect"): boolean;
|
|
||||||
emit(event: "error", error: Error): boolean;
|
|
||||||
emit(event: "exit", code: number, signal: string): boolean;
|
|
||||||
emit(event: "listening", address: Address): boolean;
|
|
||||||
emit(event: "message", message: any, handle: net.Socket | net.Server): boolean;
|
|
||||||
emit(event: "online"): boolean;
|
|
||||||
on(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
on(event: "disconnect", listener: () => void): this;
|
|
||||||
on(event: "error", listener: (error: Error) => void): this;
|
|
||||||
on(event: "exit", listener: (code: number, signal: string) => void): this;
|
|
||||||
on(event: "listening", listener: (address: Address) => void): this;
|
|
||||||
on(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
on(event: "online", listener: () => void): this;
|
|
||||||
once(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
once(event: "disconnect", listener: () => void): this;
|
|
||||||
once(event: "error", listener: (error: Error) => void): this;
|
|
||||||
once(event: "exit", listener: (code: number, signal: string) => void): this;
|
|
||||||
once(event: "listening", listener: (address: Address) => void): this;
|
|
||||||
once(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
once(event: "online", listener: () => void): this;
|
|
||||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
prependListener(event: "disconnect", listener: () => void): this;
|
|
||||||
prependListener(event: "error", listener: (error: Error) => void): this;
|
|
||||||
prependListener(event: "exit", listener: (code: number, signal: string) => void): this;
|
|
||||||
prependListener(event: "listening", listener: (address: Address) => void): this;
|
|
||||||
prependListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
prependListener(event: "online", listener: () => void): this;
|
|
||||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
prependOnceListener(event: "disconnect", listener: () => void): this;
|
|
||||||
prependOnceListener(event: "error", listener: (error: Error) => void): this;
|
|
||||||
prependOnceListener(event: "exit", listener: (code: number, signal: string) => void): this;
|
|
||||||
prependOnceListener(event: "listening", listener: (address: Address) => void): this;
|
|
||||||
prependOnceListener(event: "message", listener: (message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
prependOnceListener(event: "online", listener: () => void): this;
|
|
||||||
}
|
|
||||||
export interface Cluster extends EventEmitter {
|
|
||||||
disconnect(callback?: () => void): void;
|
|
||||||
/**
|
|
||||||
* Spawn a new worker process.
|
|
||||||
*
|
|
||||||
* This can only be called from the primary process.
|
|
||||||
* @param env Key/value pairs to add to worker process environment.
|
|
||||||
* @since v0.6.0
|
|
||||||
*/
|
|
||||||
fork(env?: any): Worker;
|
|
||||||
/** @deprecated since v16.0.0 - use isPrimary. */
|
|
||||||
readonly isMaster: boolean;
|
|
||||||
/**
|
|
||||||
* True if the process is a primary. This is determined by the `process.env.NODE_UNIQUE_ID`. If `process.env.NODE_UNIQUE_ID`
|
|
||||||
* is undefined, then `isPrimary` is `true`.
|
|
||||||
* @since v16.0.0
|
|
||||||
*/
|
|
||||||
readonly isPrimary: boolean;
|
|
||||||
/**
|
|
||||||
* True if the process is not a primary (it is the negation of `cluster.isPrimary`).
|
|
||||||
* @since v0.6.0
|
|
||||||
*/
|
|
||||||
readonly isWorker: boolean;
|
|
||||||
/**
|
|
||||||
* The scheduling policy, either `cluster.SCHED_RR` for round-robin or `cluster.SCHED_NONE` to leave it to the operating system. This is a
|
|
||||||
* global setting and effectively frozen once either the first worker is spawned, or [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings)
|
|
||||||
* is called, whichever comes first.
|
|
||||||
*
|
|
||||||
* `SCHED_RR` is the default on all operating systems except Windows. Windows will change to `SCHED_RR` once libuv is able to effectively distribute
|
|
||||||
* IOCP handles without incurring a large performance hit.
|
|
||||||
*
|
|
||||||
* `cluster.schedulingPolicy` can also be set through the `NODE_CLUSTER_SCHED_POLICY` environment variable. Valid values are `'rr'` and `'none'`.
|
|
||||||
* @since v0.11.2
|
|
||||||
*/
|
|
||||||
schedulingPolicy: number;
|
|
||||||
/**
|
|
||||||
* After calling [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings)
|
|
||||||
* (or [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv)) this settings object will contain
|
|
||||||
* the settings, including the default values.
|
|
||||||
*
|
|
||||||
* This object is not intended to be changed or set manually.
|
|
||||||
* @since v0.7.1
|
|
||||||
*/
|
|
||||||
readonly settings: ClusterSettings;
|
|
||||||
/** @deprecated since v16.0.0 - use [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings) instead. */
|
|
||||||
setupMaster(settings?: ClusterSettings): void;
|
|
||||||
/**
|
|
||||||
* `setupPrimary` is used to change the default 'fork' behavior. Once called, the settings will be present in `cluster.settings`.
|
|
||||||
*
|
|
||||||
* Any settings changes only affect future calls to [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv)
|
|
||||||
* and have no effect on workers that are already running.
|
|
||||||
*
|
|
||||||
* The only attribute of a worker that cannot be set via `.setupPrimary()` is the `env` passed to
|
|
||||||
* [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv).
|
|
||||||
*
|
|
||||||
* The defaults above apply to the first call only; the defaults for later calls are the current values at the time of
|
|
||||||
* `cluster.setupPrimary()` is called.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import cluster from 'node:cluster';
|
|
||||||
*
|
|
||||||
* cluster.setupPrimary({
|
|
||||||
* exec: 'worker.js',
|
|
||||||
* args: ['--use', 'https'],
|
|
||||||
* silent: true,
|
|
||||||
* });
|
|
||||||
* cluster.fork(); // https worker
|
|
||||||
* cluster.setupPrimary({
|
|
||||||
* exec: 'worker.js',
|
|
||||||
* args: ['--use', 'http'],
|
|
||||||
* });
|
|
||||||
* cluster.fork(); // http worker
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* This can only be called from the primary process.
|
|
||||||
* @since v16.0.0
|
|
||||||
*/
|
|
||||||
setupPrimary(settings?: ClusterSettings): void;
|
|
||||||
/**
|
|
||||||
* A reference to the current worker object. Not available in the primary process.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import cluster from 'node:cluster';
|
|
||||||
*
|
|
||||||
* if (cluster.isPrimary) {
|
|
||||||
* console.log('I am primary');
|
|
||||||
* cluster.fork();
|
|
||||||
* cluster.fork();
|
|
||||||
* } else if (cluster.isWorker) {
|
|
||||||
* console.log(`I am worker #${cluster.worker.id}`);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v0.7.0
|
|
||||||
*/
|
|
||||||
readonly worker?: Worker | undefined;
|
|
||||||
/**
|
|
||||||
* A hash that stores the active worker objects, keyed by `id` field. This makes it easy to loop through all the workers. It is only available in the primary process.
|
|
||||||
*
|
|
||||||
* A worker is removed from `cluster.workers` after the worker has disconnected _and_ exited. The order between these two events cannot be determined in advance. However, it
|
|
||||||
* is guaranteed that the removal from the `cluster.workers` list happens before the last `'disconnect'` or `'exit'` event is emitted.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* import cluster from 'node:cluster';
|
|
||||||
*
|
|
||||||
* for (const worker of Object.values(cluster.workers)) {
|
|
||||||
* worker.send('big announcement to all workers');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
* @since v0.7.0
|
|
||||||
*/
|
|
||||||
readonly workers?: NodeJS.Dict<Worker> | undefined;
|
|
||||||
readonly SCHED_NONE: number;
|
|
||||||
readonly SCHED_RR: number;
|
|
||||||
/**
|
|
||||||
* events.EventEmitter
|
|
||||||
* 1. disconnect
|
|
||||||
* 2. exit
|
|
||||||
* 3. fork
|
|
||||||
* 4. listening
|
|
||||||
* 5. message
|
|
||||||
* 6. online
|
|
||||||
* 7. setup
|
|
||||||
*/
|
|
||||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
addListener(event: "disconnect", listener: (worker: Worker) => void): this;
|
|
||||||
addListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
|
|
||||||
addListener(event: "fork", listener: (worker: Worker) => void): this;
|
|
||||||
addListener(event: "listening", listener: (worker: Worker, address: Address) => void): this;
|
|
||||||
addListener(
|
|
||||||
event: "message",
|
|
||||||
listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void,
|
|
||||||
): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
addListener(event: "online", listener: (worker: Worker) => void): this;
|
|
||||||
addListener(event: "setup", listener: (settings: ClusterSettings) => void): this;
|
|
||||||
emit(event: string | symbol, ...args: any[]): boolean;
|
|
||||||
emit(event: "disconnect", worker: Worker): boolean;
|
|
||||||
emit(event: "exit", worker: Worker, code: number, signal: string): boolean;
|
|
||||||
emit(event: "fork", worker: Worker): boolean;
|
|
||||||
emit(event: "listening", worker: Worker, address: Address): boolean;
|
|
||||||
emit(event: "message", worker: Worker, message: any, handle: net.Socket | net.Server): boolean;
|
|
||||||
emit(event: "online", worker: Worker): boolean;
|
|
||||||
emit(event: "setup", settings: ClusterSettings): boolean;
|
|
||||||
on(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
on(event: "disconnect", listener: (worker: Worker) => void): this;
|
|
||||||
on(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
|
|
||||||
on(event: "fork", listener: (worker: Worker) => void): this;
|
|
||||||
on(event: "listening", listener: (worker: Worker, address: Address) => void): this;
|
|
||||||
on(event: "message", listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
on(event: "online", listener: (worker: Worker) => void): this;
|
|
||||||
on(event: "setup", listener: (settings: ClusterSettings) => void): this;
|
|
||||||
once(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
once(event: "disconnect", listener: (worker: Worker) => void): this;
|
|
||||||
once(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
|
|
||||||
once(event: "fork", listener: (worker: Worker) => void): this;
|
|
||||||
once(event: "listening", listener: (worker: Worker, address: Address) => void): this;
|
|
||||||
once(event: "message", listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void): this; // the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
once(event: "online", listener: (worker: Worker) => void): this;
|
|
||||||
once(event: "setup", listener: (settings: ClusterSettings) => void): this;
|
|
||||||
prependListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
prependListener(event: "disconnect", listener: (worker: Worker) => void): this;
|
|
||||||
prependListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
|
|
||||||
prependListener(event: "fork", listener: (worker: Worker) => void): this;
|
|
||||||
prependListener(event: "listening", listener: (worker: Worker, address: Address) => void): this;
|
|
||||||
// the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
prependListener(
|
|
||||||
event: "message",
|
|
||||||
listener: (worker: Worker, message: any, handle?: net.Socket | net.Server) => void,
|
|
||||||
): this;
|
|
||||||
prependListener(event: "online", listener: (worker: Worker) => void): this;
|
|
||||||
prependListener(event: "setup", listener: (settings: ClusterSettings) => void): this;
|
|
||||||
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
|
|
||||||
prependOnceListener(event: "disconnect", listener: (worker: Worker) => void): this;
|
|
||||||
prependOnceListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
|
|
||||||
prependOnceListener(event: "fork", listener: (worker: Worker) => void): this;
|
|
||||||
prependOnceListener(event: "listening", listener: (worker: Worker, address: Address) => void): this;
|
|
||||||
// the handle is a net.Socket or net.Server object, or undefined.
|
|
||||||
prependOnceListener(
|
|
||||||
event: "message",
|
|
||||||
listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void,
|
|
||||||
): this;
|
|
||||||
prependOnceListener(event: "online", listener: (worker: Worker) => void): this;
|
|
||||||
prependOnceListener(event: "setup", listener: (settings: ClusterSettings) => void): this;
|
|
||||||
}
|
|
||||||
const cluster: Cluster;
|
|
||||||
export default cluster;
|
|
||||||
}
|
|
||||||
declare module "node:cluster" {
|
|
||||||
export * from "cluster";
|
|
||||||
export { default as default } from "cluster";
|
|
||||||
}
|
|
||||||
21
node_modules/@types/node/compatibility/iterators.d.ts
generated
vendored
21
node_modules/@types/node/compatibility/iterators.d.ts
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
// Backwards-compatible iterator interfaces, augmented with iterator helper methods by lib.esnext.iterator in TypeScript 5.6.
|
|
||||||
// The IterableIterator interface does not contain these methods, which creates assignability issues in places where IteratorObjects
|
|
||||||
// are expected (eg. DOM-compatible APIs) if lib.esnext.iterator is loaded.
|
|
||||||
// Also ensures that iterators returned by the Node API, which inherit from Iterator.prototype, correctly expose the iterator helper methods
|
|
||||||
// if lib.esnext.iterator is loaded.
|
|
||||||
// TODO: remove once this package no longer supports TS 5.5, and replace NodeJS.BuiltinIteratorReturn with BuiltinIteratorReturn.
|
|
||||||
|
|
||||||
// Placeholders for TS <5.6
|
|
||||||
interface IteratorObject<T, TReturn, TNext> {}
|
|
||||||
interface AsyncIteratorObject<T, TReturn, TNext> {}
|
|
||||||
|
|
||||||
declare namespace NodeJS {
|
|
||||||
// Populate iterator methods for TS <5.6
|
|
||||||
interface Iterator<T, TReturn, TNext> extends globalThis.Iterator<T, TReturn, TNext> {}
|
|
||||||
interface AsyncIterator<T, TReturn, TNext> extends globalThis.AsyncIterator<T, TReturn, TNext> {}
|
|
||||||
|
|
||||||
// Polyfill for TS 5.6's instrinsic BuiltinIteratorReturn type, required for DOM-compatible iterators
|
|
||||||
type BuiltinIteratorReturn = ReturnType<any[][typeof Symbol.iterator]> extends
|
|
||||||
globalThis.Iterator<any, infer TReturn> ? TReturn
|
|
||||||
: any;
|
|
||||||
}
|
|
||||||
452
node_modules/@types/node/console.d.ts
generated
vendored
452
node_modules/@types/node/console.d.ts
generated
vendored
@@ -1,452 +0,0 @@
|
|||||||
/**
|
|
||||||
* The `node:console` module provides a simple debugging console that is similar to
|
|
||||||
* the JavaScript console mechanism provided by web browsers.
|
|
||||||
*
|
|
||||||
* The module exports two specific components:
|
|
||||||
*
|
|
||||||
* * A `Console` class with methods such as `console.log()`, `console.error()`, and `console.warn()` that can be used to write to any Node.js stream.
|
|
||||||
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
|
|
||||||
* [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
|
|
||||||
*
|
|
||||||
* _**Warning**_: The global console object's methods are neither consistently
|
|
||||||
* synchronous like the browser APIs they resemble, nor are they consistently
|
|
||||||
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
|
|
||||||
* more information.
|
|
||||||
*
|
|
||||||
* Example using the global `console`:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.log('hello world');
|
|
||||||
* // Prints: hello world, to stdout
|
|
||||||
* console.log('hello %s', 'world');
|
|
||||||
* // Prints: hello world, to stdout
|
|
||||||
* console.error(new Error('Whoops, something bad happened'));
|
|
||||||
* // Prints error message and stack trace to stderr:
|
|
||||||
* // Error: Whoops, something bad happened
|
|
||||||
* // at [eval]:5:15
|
|
||||||
* // at Script.runInThisContext (node:vm:132:18)
|
|
||||||
* // at Object.runInThisContext (node:vm:309:38)
|
|
||||||
* // at node:internal/process/execution:77:19
|
|
||||||
* // at [eval]-wrapper:6:22
|
|
||||||
* // at evalScript (node:internal/process/execution:76:60)
|
|
||||||
* // at node:internal/main/eval_string:23:3
|
|
||||||
*
|
|
||||||
* const name = 'Will Robinson';
|
|
||||||
* console.warn(`Danger ${name}! Danger!`);
|
|
||||||
* // Prints: Danger Will Robinson! Danger!, to stderr
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Example using the `Console` class:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const out = getStreamSomehow();
|
|
||||||
* const err = getStreamSomehow();
|
|
||||||
* const myConsole = new console.Console(out, err);
|
|
||||||
*
|
|
||||||
* myConsole.log('hello world');
|
|
||||||
* // Prints: hello world, to out
|
|
||||||
* myConsole.log('hello %s', 'world');
|
|
||||||
* // Prints: hello world, to out
|
|
||||||
* myConsole.error(new Error('Whoops, something bad happened'));
|
|
||||||
* // Prints: [Error: Whoops, something bad happened], to err
|
|
||||||
*
|
|
||||||
* const name = 'Will Robinson';
|
|
||||||
* myConsole.warn(`Danger ${name}! Danger!`);
|
|
||||||
* // Prints: Danger Will Robinson! Danger!, to err
|
|
||||||
* ```
|
|
||||||
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
|
|
||||||
*/
|
|
||||||
declare module "console" {
|
|
||||||
import console = require("node:console");
|
|
||||||
export = console;
|
|
||||||
}
|
|
||||||
declare module "node:console" {
|
|
||||||
import { InspectOptions } from "node:util";
|
|
||||||
global {
|
|
||||||
// This needs to be global to avoid TS2403 in case lib.dom.d.ts is present in the same build
|
|
||||||
interface Console {
|
|
||||||
Console: console.ConsoleConstructor;
|
|
||||||
/**
|
|
||||||
* `console.assert()` writes a message if `value` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) or omitted. It only
|
|
||||||
* writes a message and does not otherwise affect execution. The output always
|
|
||||||
* starts with `"Assertion failed"`. If provided, `message` is formatted using
|
|
||||||
* [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args).
|
|
||||||
*
|
|
||||||
* If `value` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), nothing happens.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.assert(true, 'does nothing');
|
|
||||||
*
|
|
||||||
* console.assert(false, 'Whoops %s work', 'didn\'t');
|
|
||||||
* // Assertion failed: Whoops didn't work
|
|
||||||
*
|
|
||||||
* console.assert();
|
|
||||||
* // Assertion failed
|
|
||||||
* ```
|
|
||||||
* @since v0.1.101
|
|
||||||
* @param value The value tested for being truthy.
|
|
||||||
* @param message All arguments besides `value` are used as error message.
|
|
||||||
*/
|
|
||||||
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* When `stdout` is a TTY, calling `console.clear()` will attempt to clear the
|
|
||||||
* TTY. When `stdout` is not a TTY, this method does nothing.
|
|
||||||
*
|
|
||||||
* The specific operation of `console.clear()` can vary across operating systems
|
|
||||||
* and terminal types. For most Linux operating systems, `console.clear()` operates similarly to the `clear` shell command. On Windows, `console.clear()` will clear only the output in the
|
|
||||||
* current terminal viewport for the Node.js
|
|
||||||
* binary.
|
|
||||||
* @since v8.3.0
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
/**
|
|
||||||
* Maintains an internal counter specific to `label` and outputs to `stdout` the
|
|
||||||
* number of times `console.count()` has been called with the given `label`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* > console.count()
|
|
||||||
* default: 1
|
|
||||||
* undefined
|
|
||||||
* > console.count('default')
|
|
||||||
* default: 2
|
|
||||||
* undefined
|
|
||||||
* > console.count('abc')
|
|
||||||
* abc: 1
|
|
||||||
* undefined
|
|
||||||
* > console.count('xyz')
|
|
||||||
* xyz: 1
|
|
||||||
* undefined
|
|
||||||
* > console.count('abc')
|
|
||||||
* abc: 2
|
|
||||||
* undefined
|
|
||||||
* > console.count()
|
|
||||||
* default: 3
|
|
||||||
* undefined
|
|
||||||
* >
|
|
||||||
* ```
|
|
||||||
* @since v8.3.0
|
|
||||||
* @param [label='default'] The display label for the counter.
|
|
||||||
*/
|
|
||||||
count(label?: string): void;
|
|
||||||
/**
|
|
||||||
* Resets the internal counter specific to `label`.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* > console.count('abc');
|
|
||||||
* abc: 1
|
|
||||||
* undefined
|
|
||||||
* > console.countReset('abc');
|
|
||||||
* undefined
|
|
||||||
* > console.count('abc');
|
|
||||||
* abc: 1
|
|
||||||
* undefined
|
|
||||||
* >
|
|
||||||
* ```
|
|
||||||
* @since v8.3.0
|
|
||||||
* @param [label='default'] The display label for the counter.
|
|
||||||
*/
|
|
||||||
countReset(label?: string): void;
|
|
||||||
/**
|
|
||||||
* The `console.debug()` function is an alias for {@link log}.
|
|
||||||
* @since v8.0.0
|
|
||||||
*/
|
|
||||||
debug(message?: any, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* Uses [`util.inspect()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilinspectobject-options) on `obj` and prints the resulting string to `stdout`.
|
|
||||||
* This function bypasses any custom `inspect()` function defined on `obj`.
|
|
||||||
* @since v0.1.101
|
|
||||||
*/
|
|
||||||
dir(obj: any, options?: InspectOptions): void;
|
|
||||||
/**
|
|
||||||
* This method calls `console.log()` passing it the arguments received.
|
|
||||||
* This method does not produce any XML formatting.
|
|
||||||
* @since v8.0.0
|
|
||||||
*/
|
|
||||||
dirxml(...data: any[]): void;
|
|
||||||
/**
|
|
||||||
* Prints to `stderr` with newline. Multiple arguments can be passed, with the
|
|
||||||
* first used as the primary message and all additional used as substitution
|
|
||||||
* values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
|
|
||||||
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const code = 5;
|
|
||||||
* console.error('error #%d', code);
|
|
||||||
* // Prints: error #5, to stderr
|
|
||||||
* console.error('error', code);
|
|
||||||
* // Prints: error 5, to stderr
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* If formatting elements (e.g. `%d`) are not found in the first string then
|
|
||||||
* [`util.inspect()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilinspectobject-options) is called on each argument and the
|
|
||||||
* resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)
|
|
||||||
* for more information.
|
|
||||||
* @since v0.1.100
|
|
||||||
*/
|
|
||||||
error(message?: any, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* Increases indentation of subsequent lines by spaces for `groupIndentation` length.
|
|
||||||
*
|
|
||||||
* If one or more `label`s are provided, those are printed first without the
|
|
||||||
* additional indentation.
|
|
||||||
* @since v8.5.0
|
|
||||||
*/
|
|
||||||
group(...label: any[]): void;
|
|
||||||
/**
|
|
||||||
* An alias for {@link group}.
|
|
||||||
* @since v8.5.0
|
|
||||||
*/
|
|
||||||
groupCollapsed(...label: any[]): void;
|
|
||||||
/**
|
|
||||||
* Decreases indentation of subsequent lines by spaces for `groupIndentation` length.
|
|
||||||
* @since v8.5.0
|
|
||||||
*/
|
|
||||||
groupEnd(): void;
|
|
||||||
/**
|
|
||||||
* The `console.info()` function is an alias for {@link log}.
|
|
||||||
* @since v0.1.100
|
|
||||||
*/
|
|
||||||
info(message?: any, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* Prints to `stdout` with newline. Multiple arguments can be passed, with the
|
|
||||||
* first used as the primary message and all additional used as substitution
|
|
||||||
* values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
|
|
||||||
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const count = 5;
|
|
||||||
* console.log('count: %d', count);
|
|
||||||
* // Prints: count: 5, to stdout
|
|
||||||
* console.log('count:', count);
|
|
||||||
* // Prints: count: 5, to stdout
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.
|
|
||||||
* @since v0.1.100
|
|
||||||
*/
|
|
||||||
log(message?: any, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* Try to construct a table with the columns of the properties of `tabularData` (or use `properties`) and rows of `tabularData` and log it. Falls back to just
|
|
||||||
* logging the argument if it can't be parsed as tabular.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* // These can't be parsed as tabular data
|
|
||||||
* console.table(Symbol());
|
|
||||||
* // Symbol()
|
|
||||||
*
|
|
||||||
* console.table(undefined);
|
|
||||||
* // undefined
|
|
||||||
*
|
|
||||||
* console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }]);
|
|
||||||
* // ┌─────────┬─────┬─────┐
|
|
||||||
* // │ (index) │ a │ b │
|
|
||||||
* // ├─────────┼─────┼─────┤
|
|
||||||
* // │ 0 │ 1 │ 'Y' │
|
|
||||||
* // │ 1 │ 'Z' │ 2 │
|
|
||||||
* // └─────────┴─────┴─────┘
|
|
||||||
*
|
|
||||||
* console.table([{ a: 1, b: 'Y' }, { a: 'Z', b: 2 }], ['a']);
|
|
||||||
* // ┌─────────┬─────┐
|
|
||||||
* // │ (index) │ a │
|
|
||||||
* // ├─────────┼─────┤
|
|
||||||
* // │ 0 │ 1 │
|
|
||||||
* // │ 1 │ 'Z' │
|
|
||||||
* // └─────────┴─────┘
|
|
||||||
* ```
|
|
||||||
* @since v10.0.0
|
|
||||||
* @param properties Alternate properties for constructing the table.
|
|
||||||
*/
|
|
||||||
table(tabularData: any, properties?: readonly string[]): void;
|
|
||||||
/**
|
|
||||||
* Starts a timer that can be used to compute the duration of an operation. Timers
|
|
||||||
* are identified by a unique `label`. Use the same `label` when calling {@link timeEnd} to stop the timer and output the elapsed time in
|
|
||||||
* suitable time units to `stdout`. For example, if the elapsed
|
|
||||||
* time is 3869ms, `console.timeEnd()` displays "3.869s".
|
|
||||||
* @since v0.1.104
|
|
||||||
* @param [label='default']
|
|
||||||
*/
|
|
||||||
time(label?: string): void;
|
|
||||||
/**
|
|
||||||
* Stops a timer that was previously started by calling {@link time} and
|
|
||||||
* prints the result to `stdout`:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.time('bunch-of-stuff');
|
|
||||||
* // Do a bunch of stuff.
|
|
||||||
* console.timeEnd('bunch-of-stuff');
|
|
||||||
* // Prints: bunch-of-stuff: 225.438ms
|
|
||||||
* ```
|
|
||||||
* @since v0.1.104
|
|
||||||
* @param [label='default']
|
|
||||||
*/
|
|
||||||
timeEnd(label?: string): void;
|
|
||||||
/**
|
|
||||||
* For a timer that was previously started by calling {@link time}, prints
|
|
||||||
* the elapsed time and other `data` arguments to `stdout`:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.time('process');
|
|
||||||
* const value = expensiveProcess1(); // Returns 42
|
|
||||||
* console.timeLog('process', value);
|
|
||||||
* // Prints "process: 365.227ms 42".
|
|
||||||
* doExpensiveProcess2(value);
|
|
||||||
* console.timeEnd('process');
|
|
||||||
* ```
|
|
||||||
* @since v10.7.0
|
|
||||||
* @param [label='default']
|
|
||||||
*/
|
|
||||||
timeLog(label?: string, ...data: any[]): void;
|
|
||||||
/**
|
|
||||||
* Prints to `stderr` the string `'Trace: '`, followed by the [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)
|
|
||||||
* formatted message and stack trace to the current position in the code.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.trace('Show me');
|
|
||||||
* // Prints: (stack trace will vary based on where trace is called)
|
|
||||||
* // Trace: Show me
|
|
||||||
* // at repl:2:9
|
|
||||||
* // at REPLServer.defaultEval (repl.js:248:27)
|
|
||||||
* // at bound (domain.js:287:14)
|
|
||||||
* // at REPLServer.runBound [as eval] (domain.js:300:12)
|
|
||||||
* // at REPLServer.<anonymous> (repl.js:412:12)
|
|
||||||
* // at emitOne (events.js:82:20)
|
|
||||||
* // at REPLServer.emit (events.js:169:7)
|
|
||||||
* // at REPLServer.Interface._onLine (readline.js:210:10)
|
|
||||||
* // at REPLServer.Interface._line (readline.js:549:8)
|
|
||||||
* // at REPLServer.Interface._ttyWrite (readline.js:826:14)
|
|
||||||
* ```
|
|
||||||
* @since v0.1.104
|
|
||||||
*/
|
|
||||||
trace(message?: any, ...optionalParams: any[]): void;
|
|
||||||
/**
|
|
||||||
* The `console.warn()` function is an alias for {@link error}.
|
|
||||||
* @since v0.1.100
|
|
||||||
*/
|
|
||||||
warn(message?: any, ...optionalParams: any[]): void;
|
|
||||||
// --- Inspector mode only ---
|
|
||||||
/**
|
|
||||||
* This method does not display anything unless used in the inspector. The `console.profile()`
|
|
||||||
* method starts a JavaScript CPU profile with an optional label until {@link profileEnd}
|
|
||||||
* is called. The profile is then added to the Profile panel of the inspector.
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.profile('MyLabel');
|
|
||||||
* // Some code
|
|
||||||
* console.profileEnd('MyLabel');
|
|
||||||
* // Adds the profile 'MyLabel' to the Profiles panel of the inspector.
|
|
||||||
* ```
|
|
||||||
* @since v8.0.0
|
|
||||||
*/
|
|
||||||
profile(label?: string): void;
|
|
||||||
/**
|
|
||||||
* This method does not display anything unless used in the inspector. Stops the current
|
|
||||||
* JavaScript CPU profiling session if one has been started and prints the report to the
|
|
||||||
* Profiles panel of the inspector. See {@link profile} for an example.
|
|
||||||
*
|
|
||||||
* If this method is called without a label, the most recently started profile is stopped.
|
|
||||||
* @since v8.0.0
|
|
||||||
*/
|
|
||||||
profileEnd(label?: string): void;
|
|
||||||
/**
|
|
||||||
* This method does not display anything unless used in the inspector. The `console.timeStamp()`
|
|
||||||
* method adds an event with the label `'label'` to the Timeline panel of the inspector.
|
|
||||||
* @since v8.0.0
|
|
||||||
*/
|
|
||||||
timeStamp(label?: string): void;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The `console` module provides a simple debugging console that is similar to the
|
|
||||||
* JavaScript console mechanism provided by web browsers.
|
|
||||||
*
|
|
||||||
* The module exports two specific components:
|
|
||||||
*
|
|
||||||
* * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
|
|
||||||
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
|
|
||||||
* [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
|
|
||||||
*
|
|
||||||
* _**Warning**_: The global console object's methods are neither consistently
|
|
||||||
* synchronous like the browser APIs they resemble, nor are they consistently
|
|
||||||
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
|
|
||||||
* more information.
|
|
||||||
*
|
|
||||||
* Example using the global `console`:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* console.log('hello world');
|
|
||||||
* // Prints: hello world, to stdout
|
|
||||||
* console.log('hello %s', 'world');
|
|
||||||
* // Prints: hello world, to stdout
|
|
||||||
* console.error(new Error('Whoops, something bad happened'));
|
|
||||||
* // Prints error message and stack trace to stderr:
|
|
||||||
* // Error: Whoops, something bad happened
|
|
||||||
* // at [eval]:5:15
|
|
||||||
* // at Script.runInThisContext (node:vm:132:18)
|
|
||||||
* // at Object.runInThisContext (node:vm:309:38)
|
|
||||||
* // at node:internal/process/execution:77:19
|
|
||||||
* // at [eval]-wrapper:6:22
|
|
||||||
* // at evalScript (node:internal/process/execution:76:60)
|
|
||||||
* // at node:internal/main/eval_string:23:3
|
|
||||||
*
|
|
||||||
* const name = 'Will Robinson';
|
|
||||||
* console.warn(`Danger ${name}! Danger!`);
|
|
||||||
* // Prints: Danger Will Robinson! Danger!, to stderr
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Example using the `Console` class:
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* const out = getStreamSomehow();
|
|
||||||
* const err = getStreamSomehow();
|
|
||||||
* const myConsole = new console.Console(out, err);
|
|
||||||
*
|
|
||||||
* myConsole.log('hello world');
|
|
||||||
* // Prints: hello world, to out
|
|
||||||
* myConsole.log('hello %s', 'world');
|
|
||||||
* // Prints: hello world, to out
|
|
||||||
* myConsole.error(new Error('Whoops, something bad happened'));
|
|
||||||
* // Prints: [Error: Whoops, something bad happened], to err
|
|
||||||
*
|
|
||||||
* const name = 'Will Robinson';
|
|
||||||
* myConsole.warn(`Danger ${name}! Danger!`);
|
|
||||||
* // Prints: Danger Will Robinson! Danger!, to err
|
|
||||||
* ```
|
|
||||||
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
|
|
||||||
*/
|
|
||||||
namespace console {
|
|
||||||
interface ConsoleConstructorOptions {
|
|
||||||
stdout: NodeJS.WritableStream;
|
|
||||||
stderr?: NodeJS.WritableStream | undefined;
|
|
||||||
/**
|
|
||||||
* Ignore errors when writing to the underlying streams.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
ignoreErrors?: boolean | undefined;
|
|
||||||
/**
|
|
||||||
* Set color support for this `Console` instance. Setting to true enables coloring while inspecting
|
|
||||||
* values. Setting to `false` disables coloring while inspecting values. Setting to `'auto'` makes color
|
|
||||||
* support depend on the value of the `isTTY` property and the value returned by `getColorDepth()` on the
|
|
||||||
* respective stream. This option can not be used, if `inspectOptions.colors` is set as well.
|
|
||||||
* @default auto
|
|
||||||
*/
|
|
||||||
colorMode?: boolean | "auto" | undefined;
|
|
||||||
/**
|
|
||||||
* Specifies options that are passed along to
|
|
||||||
* [`util.inspect()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilinspectobject-options).
|
|
||||||
*/
|
|
||||||
inspectOptions?: InspectOptions | undefined;
|
|
||||||
/**
|
|
||||||
* Set group indentation.
|
|
||||||
* @default 2
|
|
||||||
*/
|
|
||||||
groupIndentation?: number | undefined;
|
|
||||||
}
|
|
||||||
interface ConsoleConstructor {
|
|
||||||
prototype: Console;
|
|
||||||
new(stdout: NodeJS.WritableStream, stderr?: NodeJS.WritableStream, ignoreErrors?: boolean): Console;
|
|
||||||
new(options: ConsoleConstructorOptions): Console;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var console: Console;
|
|
||||||
}
|
|
||||||
export = globalThis.console;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user