init commit

This commit is contained in:
2025-10-04 11:55:35 +09:00
parent 9951d8367f
commit b1de55d253
1306 changed files with 8820 additions and 188539 deletions

6
.idea/AndroidProjectSystem.xml generated Normal file
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View 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
View 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
View 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>

View 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

View 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

View 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

View 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

View 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")
}

View File

@@ -1,19 +1,18 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.godeye"
compileSdk = 36
compileSdk = 29 // Понижаем до Android 10
defaultConfig {
applicationId = "com.example.godeye"
minSdk = 24
targetSdk = 36
targetSdk = 28 // Понижаем до Android 9
versionCode = 1
versionName = "1.0"
versionName = "1.0-legacy"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -27,79 +26,62 @@ android {
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
viewBinding = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// Исправляем проблему с Java toolchain
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
kotlinOptions {
jvmTarget = "1.8"
}
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
buildFeatures {
compose = false
viewBinding = true
}
}
dependencies {
// Core Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
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)
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для Android 9 (compileSdk 29)
implementation("androidx.core:core-ktx:1.3.2") // Совместимо с API 29
implementation("androidx.appcompat:appcompat:1.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.2.0") // Совместимо с API 29
// ViewModel and LiveData
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.activity:activity-ktx:1.8.2")
// Классический Android UI - версии для API 29
implementation("com.google.android.material:material:1.3.0") // Совместимо с API 29
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.fragment:fragment-ktx:1.2.5") // Совместимо с API 29
implementation("androidx.cardview:cardview:1.0.0") // Совместимо с API 29
implementation("androidx.activity:activity-ktx:1.1.0") // Совместимо с API 29
// Socket.IO для WebSocket соединения
implementation("io.socket:socket.io-client:2.1.2")
// СТАРЫЕ ViewModel версии для API 29
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") // Совместимо с API 29
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") // Совместимо с API 29
// Пока уберем WebRTC зависимость - создадим заглушку для демонстрации
// В реальном проекте нужно будет настроить правильную WebRTC библиотеку
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
// Вместо CameraX используем старую Camera2 API напрямую
// Camera2 API
implementation("androidx.camera:camera-core:1.3.1")
implementation("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")
// Socket.IO и базовые сетевые библиотеки
implementation("io.socket:socket.io-client:2.1.0")
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
// JSON парсинг
implementation("com.google.code.gson:gson:2.10.1")
// УБИРАЕМ WebRTC полностью для стабильности
// 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 для фоновых задач
implementation("androidx.work:work-runtime-ktx:2.9.0")
// Permissions
implementation("androidx.activity:activity-compose:1.8.2")
// Navigation
implementation("androidx.navigation:navigation-compose:2.7.6")
// УБИРАЕМ Work Manager и Activity KTX
// implementation("androidx.work:work-runtime-ktx:2.8.1")
// implementation("androidx.activity:activity-ktx:1.7.2")
// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
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)
}
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
}

View 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>

View File

@@ -2,56 +2,67 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Основные разрешения -->
<!-- Разрешения согласно ТЗ -->
<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" />
<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.autofocus" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application
android:name=".GodEyeApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
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:theme="@style/Theme.GodEye"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- MainActivity - главный экран согласно ТЗ -->
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GodEye"
android:screenOrientation="portrait">
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>
<!-- 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
android:name=".services.SocketService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="camera" />
android:exported="false" />
<!-- Camera Service для работы с камерой -->
<service
android:name=".services.CameraService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="camera" />
</application>
</manifest>
</manifest>

View 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()
}
}

View 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)
}
}
}

View 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)
}
}
}

View File

@@ -1,118 +1,431 @@
package com.example.godeye
import android.Manifest
import android.content.pm.PackageManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.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.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.viewmodels.MainViewModel
import com.example.godeye.utils.ErrorHandler
import com.example.godeye.utils.Logger
import kotlinx.coroutines.launch
/**
* MainActivity - упрощенная версия для Android 9
* БЕЗ сложных анимаций и градиентов
*/
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var permissionManager: PermissionManager
private val errorHandler = ErrorHandler()
private var socketService: SocketService? = null
// Launcher для запроса разрешений
private val permissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) {
Logger.d("All permissions granted")
viewModel.startServices() // Запуск сервисов после получения разрешений
} else {
Logger.w("Some permissions were denied")
val deniedPermissions = permissions.filterValues { !it }.keys
Logger.w("Denied permissions: ${deniedPermissions.joinToString(", ")}")
// Подключение к SocketService
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Logger.step("SERVICE_CONNECTED", "SocketService connected to MainActivity")
val binder = service as SocketService.LocalBinder
socketService = binder.getService()
viewModel.bindToSocketService(socketService!!)
}
// Логируем статус разрешений
permissionManager.logPermissionsStatus()
override fun onServiceDisconnected(name: ComponentName?) {
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?) {
super.onCreate(savedInstanceState)
Logger.d("MainActivity created")
permissionManager = PermissionManager(this)
try {
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}")
// Проверяем разрешения при запуске
checkAndRequestPermissions()
if (permissionManager.hasAllRequiredPermissions()) {
viewModel.startServices() // Запуск сервисов если разрешения уже есть
}
// Запуск SocketService
startAndBindSocketService()
setContent {
GodEyeTheme {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
setContent {
GodEyeTheme {
var showSettings by remember { mutableStateOf(false) }
var showCamera by remember { mutableStateOf(false) }
val cameraRequest by viewModel.cameraRequest.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
MainScreen(
viewModel = viewModel,
onRequestPermissions = {
requestMissingPermissions()
},
onShowError = { message ->
coroutineScope.launch {
snackbarHostState.showSnackbar(message)
}
// Автоматическое принятие запросов камеры
LaunchedEffect(cameraRequest) {
val currentRequest = cameraRequest
if (currentRequest != null) {
Logger.step("AUTO_ACCEPT_CAMERA_REQUEST", "Auto-accepting camera request")
showCamera = true
viewModel.acceptCameraRequest(currentRequest.sessionId, "Auto-accepted")
} else {
showCamera = false
}
}
// Обработка ошибок
val connectionState by viewModel.connectionState.collectAsState()
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() {
Logger.step("ACTIVITY_DESTROY", "MainActivity destroyed")
try {
unbindService(serviceConnection)
} catch (e: Exception) {
Logger.error("UNBIND_SERVICE_ERROR", "Error unbinding SocketService", e)
}
super.onDestroy()
Logger.d("MainActivity destroyed")
}
}
}

View 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()
}
}

View 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
)
}
}
}
}

View 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)
}
}
}

View 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)
}
}
}
}
}
}

View 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()
}
}

View File

@@ -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
}
}

View File

@@ -4,99 +4,218 @@ import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import com.example.godeye.models.AppError
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) {
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 {
/**
* Все необходимые разрешения согласно ТЗ
*/
val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.WAKE_LOCK,
Manifest.permission.FOREGROUND_SERVICE,
Manifest.permission.POST_NOTIFICATIONS
)
val CAMERA_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
}
/**
* Проверить, есть ли все необходимые разрешения
*/
fun hasAllRequiredPermissions(): Boolean {
return REQUIRED_PERMISSIONS.all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
Manifest.permission.FOREGROUND_SERVICE
).apply {
// Добавляем FOREGROUND_SERVICE_CAMERA для API 34+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
}
}
}
/**
* Проверить разрешения для камеры
*/
fun hasCameraPermissions(): Boolean {
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(
/**
* Критически важные разрешения для основной функциональности
*/
val CRITICAL_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
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() {
Logger.d("=== Permission Status ===")
REQUIRED_PERMISSIONS.forEach { permission ->
val granted = hasPermission(permission)
Logger.d("$permission: ${if (granted) "GRANTED" else "DENIED"}")
fun getPermissionDescription(permission: String): String {
return when (permission) {
Manifest.permission.CAMERA -> "Доступ к камере для видеосвязи"
Manifest.permission.RECORD_AUDIO -> "Доступ к микрофону для аудиосвязи"
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()
}

View File

@@ -1,24 +1,33 @@
package com.example.godeye.managers
import com.example.godeye.models.CameraSession
import com.example.godeye.models.*
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.ConcurrentHashMap
/**
* Менеджер для управления активными сессиями с операторами
* SessionManager - управление активными сессиями с операторами
* Отслеживает состояние WebRTC соединений и сессий камеры
*/
class SessionManager {
private val _activeSessions = MutableStateFlow<List<CameraSession>>(emptyList())
val activeSessions: StateFlow<List<CameraSession>> = _activeSessions.asStateFlow()
private val activeSessions = ConcurrentHashMap<String, CameraSession>()
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) {
val newSession = CameraSession(
fun createSession(sessionId: String, operatorId: String, cameraType: String): CameraSession {
Logger.step("SESSION_CREATE", "Creating session: $sessionId for operator $operatorId")
val session = CameraSession(
sessionId = sessionId,
operatorId = operatorId,
cameraType = cameraType,
@@ -27,126 +36,121 @@ class SessionManager {
webRTCConnected = false
)
val currentSessions = _activeSessions.value.toMutableList()
// Удаляем существующую сессию с тем же ID, если есть
currentSessions.removeAll { it.sessionId == sessionId }
currentSessions.add(newSession)
_activeSessions.value = currentSessions
activeSessions[sessionId] = session
updateSessionsFlow()
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) {
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
fun updateWebRTCConnection(sessionId: String, connected: Boolean) {
activeSessions[sessionId]?.let { session ->
session.webRTCConnected = connected
activeSessions[sessionId] = session
updateSessionsFlow()
if (sessionIndex != -1) {
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
webRTCConnected = connected
)
_activeSessions.value = currentSessions
Logger.d("WebRTC status updated for session $sessionId: $connected")
Logger.step("SESSION_WEBRTC_UPDATED",
"Session $sessionId WebRTC status updated: $connected")
}
}
/**
* Переключить камеру для сессии
* Завершение сессии
*/
fun switchCameraForSession(sessionId: String, newCameraType: String) {
val currentSessions = _activeSessions.value.toMutableList()
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
fun endSession(sessionId: String, reason: String = "User ended") {
activeSessions[sessionId]?.let { session ->
session.isActive = false
activeSessions.remove(sessionId)
updateSessionsFlow()
if (sessionIndex != -1) {
currentSessions[sessionIndex] = currentSessions[sessionIndex].copy(
cameraType = newCameraType
)
_activeSessions.value = currentSessions
Logger.d("Camera switched for session $sessionId to $newCameraType")
Logger.step("SESSION_ENDED", "Session ended: $sessionId, reason: $reason")
}
}
/**
* Завершить сессию
*/
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
* Получение активной сессии по ID
*/
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 {
return _activeSessions.value.isNotEmpty()
return activeSessions.values.any { it.isActive }
}
/**
* Получить количество активных сессий
* Завершение всех активных сессий
*/
fun getActiveSessionCount(): Int {
return _activeSessions.value.size
fun endAllSessions(reason: String = "Service stopped") {
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() {
val sessionIds = _activeSessions.value.map { it.sessionId }
_activeSessions.value = emptyList()
Logger.d("All sessions ended: ${sessionIds.joinToString(", ")}")
fun switchCamera(sessionId: String, newCameraType: String) {
activeSessions[sessionId]?.let { session ->
session.cameraType = newCameraType
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 {
val sessions = _activeSessions.value
val active = activeSessions.values.filter { it.isActive }
val withWebRTC = active.filter { it.webRTCConnected }
return SessionStats(
totalSessions = sessions.size,
connectedSessions = sessions.count { it.webRTCConnected },
activeSessions = sessions.count { it.isActive },
oldestSessionTime = sessions.minOfOrNull { it.startTime },
newestSessionTime = sessions.maxOfOrNull { it.startTime }
totalActive = active.size,
webRTCConnected = withWebRTC.size,
operators = active.map { it.operatorId }.distinct().size,
averageDuration = if (active.isNotEmpty()) {
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(
val totalSessions: Int,
val connectedSessions: Int,
val activeSessions: Int,
val oldestSessionTime: Long?,
val newestSessionTime: Long?
val totalActive: Int,
val webRTCConnected: Int,
val operators: Int,
val averageDuration: Long
)

View File

@@ -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
}
}

View 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> // Фокусные расстояния для определения типа
)

View File

@@ -1,154 +1,55 @@
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 {
DISCONNECTED,
CONNECTING,
CONNECTED,
ERROR,
RECONNECTING
RECONNECTING,
ERROR
}
/**
* Состояния WebRTC соединения
*/
enum class WebRTCConnectionState {
NEW,
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 CameraResponse(
val sessionId: String,
val accepted: Boolean,
val reason: String? = null,
val streamUrl: String? = 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"
}

View 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()
}

View File

@@ -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")
}
}

View File

@@ -1,19 +1,17 @@
package com.example.godeye.services
import android.app.*
import android.content.Context
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.example.godeye.MainActivity
import com.example.godeye.R
import com.example.godeye.managers.PermissionManager
import com.example.godeye.models.*
import com.example.godeye.utils.Constants
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.JsonObject
import io.socket.client.IO
@@ -25,11 +23,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONObject
import java.net.URI
/**
* Сервис для управления WebSocket соединением с backend сервером
* SocketService - основной сервис для WebSocket соединения с backend сервером
* Работает в фоне и обеспечивает постоянное соединение с сервером
*/
class SocketService : Service() {
@@ -38,393 +38,359 @@ class SocketService : Service() {
private val gson = Gson()
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var permissionManager: PermissionManager
// StateFlows для отслеживания состояния
// Состояния сервиса
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private val _deviceId = MutableStateFlow("")
val deviceId: StateFlow<String> = _deviceId.asStateFlow()
private val _error = MutableStateFlow<AppError?>(null)
val error: StateFlow<AppError?> = _error.asStateFlow()
// События для передачи в UI
private val _cameraRequests = MutableStateFlow<CameraRequest?>(null)
val cameraRequests: StateFlow<CameraRequest?> = _cameraRequests.asStateFlow()
// События для UI
private val _cameraRequest = MutableStateFlow<CameraRequest?>(null)
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()
private val _webRTCEvents = MutableStateFlow<WebRTCEvent?>(null)
val webRTCEvents: StateFlow<WebRTCEvent?> = _webRTCEvents.asStateFlow()
inner class LocalBinder : Binder() {
fun getService(): SocketService = this@SocketService
}
override fun onBind(intent: Intent): IBinder = binder
override fun onCreate() {
super.onCreate()
Logger.d("SocketService created")
permissionManager = PermissionManager(this)
_deviceId.value = generateDeviceId()
Logger.step("SOCKET_SERVICE_CREATE", "SocketService created")
createNotificationChannel()
startForeground(NOTIFICATION_ID, createNotification())
}
override fun onBind(intent: Intent?): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(Constants.FOREGROUND_SERVICE_ID, createNotification())
return START_STICKY
Logger.step("SOCKET_SERVICE_START", "SocketService started")
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 {
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)
Logger.d("URI created successfully: $uri")
Logger.d("Creating Socket.IO client")
val options = IO.Options().apply {
timeout = 10000 // Увеличиваем таймаут до 10 секунд
timeout = 10000
reconnection = true
reconnectionDelay = 2000 // Увеличиваем задержку между попытками
reconnectionAttempts = 3 // Уменьшаем количество попыток
forceNew = true // Принудительно создаваем новое соединение
reconnectionAttempts = 5
reconnectionDelay = 1000
}
socket = IO.socket(uri, options).apply {
Logger.d("Socket.IO client created, setting up listeners")
setupEventListeners()
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()
}
}
socket = IO.socket(uri, options)
setupEventListeners()
socket?.connect()
} 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
_error.value = AppError.SocketError("Connection failed: ${e.message}")
}
}
}
/**
* Отключиться от сервера
*/
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
* Настройка обработчиков событий Socket.IO
*/
private fun setupEventListeners() {
socket?.apply {
Logger.d("Setting up Socket.IO event listeners")
// Событие подключения
on(Socket.EVENT_CONNECT) {
Logger.d("✅ Socket connected successfully")
Logger.step("SOCKET_CONNECTED", "Connected to server")
_connectionState.value = ConnectionState.CONNECTED
registerDevice()
}
on(Socket.EVENT_DISCONNECT) { args ->
val reason = args.firstOrNull()?.toString() ?: "unknown"
Logger.d("❌ Socket disconnected: $reason")
// Событие отключения
on(Socket.EVENT_DISCONNECT) {
Logger.step("SOCKET_DISCONNECTED", "Disconnected from server")
_connectionState.value = ConnectionState.DISCONNECTED
}
on(Socket.EVENT_CONNECT_ERROR) { args ->
val error = args.firstOrNull()?.toString() ?: "Unknown connection error"
Logger.e("🔥 Socket connection error: $error")
_connectionState.value = ConnectionState.ERROR
_error.value = AppError.SocketError(error)
// Событие подключения (исправлено)
on(Socket.EVENT_CONNECT) {
Logger.step("SOCKET_RECONNECTED", "Reconnected to server")
_connectionState.value = ConnectionState.CONNECTED
registerDevice()
}
on(Constants.SocketEvents.REGISTER_SUCCESS) { args ->
Logger.d("Device registered successfully")
val data = args.firstOrNull()?.toString()
Logger.d("Registration response: $data")
// Успешная регистрация устройства
on("register:success") { args ->
Logger.step("REGISTER_SUCCESS", "Device registered successfully")
val response = args[0] as JSONObject
Logger.d("Registration response: $response")
}
on(Constants.SocketEvents.REGISTER_ERROR) { args ->
val error = args.firstOrNull()?.toString() ?: "Registration failed"
Logger.e("Device registration error: $error")
_error.value = AppError.SocketError(error)
}
on(Constants.SocketEvents.CAMERA_REQUEST) { args ->
// Запрос доступа к камере от оператора
on("camera:request") { args ->
try {
val data = JSONObject(args[0].toString())
val request = CameraRequest(
sessionId = data.getString("sessionId"),
operatorId = data.getString("operatorId"),
cameraType = data.getString("cameraType")
val requestData = args[0] as JSONObject
val cameraRequest = CameraRequest(
sessionId = requestData.getString("sessionId"),
operatorId = requestData.getString("operatorId"),
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) {
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 {
val data = JSONObject(args[0].toString())
val data = args[0] as JSONObject
val sessionId = data.getString("sessionId")
Logger.d("Camera disconnect received for session: $sessionId")
_sessionDisconnect.value = sessionId
Logger.step("CAMERA_DISCONNECT", "Session $sessionId ended by operator")
// Очистить текущий запрос если он совпадает
if (_cameraRequests.value?.sessionId == sessionId) {
_cameraRequests.value = null
}
} 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 {
val data = JSONObject(args[0].toString())
val sessionId = data.getString("sessionId")
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(
val data = args[0] as JSONObject
val event = WebRTCEvent.Offer(
sessionId = data.getString("sessionId"),
type = "offer",
sdp = data.getString("offer")
offer = 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) {
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 {
val data = JSONObject(args[0].toString())
val message = WebRTCMessage(
val data = args[0] as JSONObject
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"),
type = "ice-candidate",
candidate = data.getString("candidate"),
sdpMid = data.getString("sdpMid"),
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) {
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() {
try {
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
val deviceInfo = DeviceInfo(
availableCameras = cameraManager.getAvailableCameraTypes()
)
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 deviceInfo = DeviceInfo(
model = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
androidVersion = android.os.Build.VERSION.RELEASE,
appVersion = "1.0.0",
availableCameras = listOf("back", "front", "ultra_wide", "telephoto") // Получить из CameraManager
)
val statusText = when (_connectionState.value) {
ConnectionState.CONNECTED -> "Подключено"
ConnectionState.CONNECTING -> "Подключение..."
ConnectionState.RECONNECTING -> "Переподключение..."
ConnectionState.DISCONNECTED -> "Отключено"
ConnectionState.ERROR -> "Ошибка подключения"
val registerData = JSONObject().apply {
put("deviceId", _deviceId.value)
put("deviceInfo", JSONObject().apply {
put("model", deviceInfo.model)
put("androidVersion", deviceInfo.androidVersion)
put("appVersion", deviceInfo.appVersion)
put("availableCameras", JSONArray().apply {
deviceInfo.availableCameras.forEach { put(it) }
})
})
}
return NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ID)
.setContentTitle("GodEye Signal Center")
.setContentText("Статус: $statusText")
socket?.emit("register:android", registerData)
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.model}")
}
/**
* Отправка ответа на запрос камеры
*/
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)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.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() {
super.onDestroy()
Logger.step("SOCKET_SERVICE_DESTROY", "SocketService destroyed")
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()
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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)
)
}
}

View File

@@ -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("Запустить")
}
}
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -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)"
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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)

View 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
)
}

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -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()
}

View File

@@ -1,49 +1,91 @@
package com.example.godeye.utils
/**
* 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 {
const val BACK = "back"
const val FRONT = "front"
const val WIDE = "wide"
const val ULTRA_WIDE = "ultra_wide"
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 {
const val SERVER_URL = "server_url"
const val DEVICE_ID = "device_id"
const val AUTO_ACCEPT_REQUESTS = "auto_accept_requests"
const val CAMERA_QUALITY = "camera_quality"
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 FOREGROUND_SERVICE_ID = 1001
const val SERVICE_NOTIFICATION_ID = 1001
const val CAMERA_REQUEST_NOTIFICATION_ID = 1002
// WebRTC настройки
val STUN_SERVERS = listOf(
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302"
)
// Таймауты
const val CAMERA_OPEN_TIMEOUT = 2500L
const val WEBRTC_CONNECTION_TIMEOUT = 10000L
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
}
}

View 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()
}
}
}

View File

@@ -2,139 +2,12 @@ package com.example.godeye.utils
import android.content.Context
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.*
/**
* Расширения для Context
*/
fun Context.getPreferences(): SharedPreferences {
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
}
fun Context.generateDeviceId(): String {
val prefs = getPreferences()
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)
}
fun generateDeviceId(): String {
return "android_${UUID.randomUUID().toString().take(8)}"
}

View 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()
}
}

View 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) {}
}

View 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>

View 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>

View 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>

View File

@@ -1,80 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">GodEye Signal Center</string>
<!-- Главный экран -->
<string name="device_id_label">ID устройства:</string>
<string name="server_url_label">URL сервера:</string>
<string name="server_url_hint">http://192.168.1.100:3001</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="connection_status">Статус подключения</string>
<string name="connect">Подключиться</string>
<string name="disconnect">Отключиться</string>
<string name="server_url">URL сервера</string>
<string name="device_info">Информация об устройстве</string>
<string name="camera_request_title">Запрос доступа к камере</string>
<string name="camera_request_message">Оператор %1$s запрашивает доступ к камере %2$s</string>
<string name="session_id_label">ID сессии: %1$s</string>
<string name="allow_button">Разрешить</string>
<string name="deny_button">Отклонить</string>
<string name="remember_choice">Запомнить для этого оператора</string>
<string name="allow">Разрешить</string>
<string name="deny">Отклонить</string>
<string name="active_sessions">Активные сессии</string>
<string name="no_sessions">Нет активных сессий</string>
<string name="waiting_for_requests">Ожидание запросов операторов</string>
<string name="device_ready">Устройство готово к приему сессий</string>
<string name="permissions_required">Требуются разрешения</string>
<string name="grant_permissions">Предоставить разрешения</string>
</resources>
<!-- Типы камер -->
<string name="camera_type_back">Основная</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>

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.GodEye" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.GodEye" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>

View 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
View 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
View File

@@ -1 +0,0 @@
../mime/cli.js

1
node_modules/.bin/nodemon generated vendored
View File

@@ -1 +0,0 @@
../nodemon/bin/nodemon.js

1
node_modules/.bin/nodetouch generated vendored
View File

@@ -1 +0,0 @@
../touch/bin/nodetouch.js

1
node_modules/.bin/semver generated vendored
View File

@@ -1 +0,0 @@
../semver/bin/semver.js

1316
node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -1,4 +0,0 @@
{
"name": "@socket.io/component-emitter",
"type": "commonjs"
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -1,4 +0,0 @@
{
"name": "@socket.io/component-emitter",
"type": "module"
}

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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;

View File

@@ -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
View File

@@ -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
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

579
node_modules/@types/node/cluster.d.ts generated vendored
View File

@@ -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";
}

View File

@@ -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
View File

@@ -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