diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 0000000..371f2e2
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml
new file mode 100644
index 0000000..4ea72a9
--- /dev/null
+++ b/.idea/copilot.data.migration.agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml
new file mode 100644
index 0000000..7ef04e2
--- /dev/null
+++ b/.idea/copilot.data.migration.ask.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml
new file mode 100644
index 0000000..1f2ea11
--- /dev/null
+++ b/.idea/copilot.data.migration.ask2agent.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml
new file mode 100644
index 0000000..8648f94
--- /dev/null
+++ b/.idea/copilot.data.migration.edit.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..871e2b2
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..b2c751a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.kotlin/errors/errors-1759321347725.log b/.kotlin/errors/errors-1759321347725.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1759321347725.log
@@ -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
+
diff --git a/.kotlin/errors/errors-1759400006923.log b/.kotlin/errors/errors-1759400006923.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1759400006923.log
@@ -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
+
diff --git a/.kotlin/errors/errors-1759466115565.log b/.kotlin/errors/errors-1759466115565.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1759466115565.log
@@ -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
+
diff --git a/.kotlin/errors/errors-1759530749084.log b/.kotlin/errors/errors-1759530749084.log
new file mode 100644
index 0000000..1219b50
--- /dev/null
+++ b/.kotlin/errors/errors-1759530749084.log
@@ -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
+
diff --git a/app/build-legacy.gradle.kts b/app/build-legacy.gradle.kts
new file mode 100644
index 0000000..5fa213c
--- /dev/null
+++ b/app/build-legacy.gradle.kts
@@ -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")
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b8453d7..184736c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
-}
\ No newline at end of file
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
+}
diff --git a/app/src/main/AndroidManifest-legacy.xml b/app/src/main/AndroidManifest-legacy.xml
new file mode 100644
index 0000000..3881c7e
--- /dev/null
+++ b/app/src/main/AndroidManifest-legacy.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5e388e4..6933832 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,56 +2,67 @@
-
+
-
-
-
+
+ android:theme="@style/Theme.GodEye"
+ android:networkSecurityConfig="@xml/network_security_config"
+ android:usesCleartextTraffic="true"
+ tools:targetApi="31">
+
+ android:screenOrientation="portrait"
+ android:launchMode="singleTop">
-
+
+
+
+
+
+
+
+ android:exported="false" />
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/example/godeye/GodEyeApplication.kt b/app/src/main/java/com/example/godeye/GodEyeApplication.kt
new file mode 100644
index 0000000..d661b90
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/GodEyeApplication.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt b/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
new file mode 100644
index 0000000..065ca7a
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/LegacyCameraActivity.kt
@@ -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, 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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/LegacyMainActivity.kt b/app/src/main/java/com/example/godeye/LegacyMainActivity.kt
new file mode 100644
index 0000000..1ac50f6
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/LegacyMainActivity.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/MainActivity.kt b/app/src/main/java/com/example/godeye/MainActivity.kt
index fce2244..528e40c 100644
--- a/app/src/main/java/com/example/godeye/MainActivity.kt
+++ b/app/src/main/java/com/example/godeye/MainActivity.kt
@@ -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")
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/godeye/MainViewModel.kt b/app/src/main/java/com/example/godeye/MainViewModel.kt
new file mode 100644
index 0000000..014a56b
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/MainViewModel.kt
@@ -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()
+
+ // Управляющие компоненты согласно ТЗ
+ 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.asStateFlow()
+
+ private val _serverUrl = MutableStateFlow("")
+ val serverUrl: StateFlow = _serverUrl.asStateFlow()
+
+ private val _deviceId = MutableStateFlow("")
+ val deviceId: StateFlow = _deviceId.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: StateFlow = _isLoading.asStateFlow()
+
+ // Управление сессиями согласно ТЗ
+ private val _cameraRequest = MutableStateFlow(null)
+ val cameraRequest: StateFlow = _cameraRequest.asStateFlow()
+
+ private val _activeSessions = MutableStateFlow>(emptyMap())
+ val activeSessions: StateFlow> = _activeSessions.asStateFlow()
+
+ private val _isStreaming = MutableStateFlow(false)
+ val isStreaming: StateFlow = _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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/SettingsScreen.kt b/app/src/main/java/com/example/godeye/SettingsScreen.kt
new file mode 100644
index 0000000..a3ce94e
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/SettingsScreen.kt
@@ -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
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/camera/CameraManager.kt b/app/src/main/java/com/example/godeye/camera/CameraManager.kt
new file mode 100644
index 0000000..c7930f4
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/camera/CameraManager.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/camera/CameraScreen.kt b/app/src/main/java/com/example/godeye/camera/CameraScreen.kt
new file mode 100644
index 0000000..06d3323
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/camera/CameraScreen.kt
@@ -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(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)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt b/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
new file mode 100644
index 0000000..cef5975
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/managers/Camera2Manager.kt
@@ -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.asStateFlow()
+
+ private val _availableCameras = MutableStateFlow>(emptyList())
+ val availableCameras: StateFlow> = _availableCameras.asStateFlow()
+
+ init {
+ detectAvailableCameras()
+ }
+
+ /**
+ * Определение доступных камер устройства согласно ТЗ
+ * Поддерживает: back, front, wide, telephoto
+ */
+ private fun detectAvailableCameras() {
+ Logger.step("CAMERA_DETECTION", "Detecting available cameras")
+
+ val cameras = mutableListOf()
+
+ 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 {
+ 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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/managers/CameraManager.kt b/app/src/main/java/com/example/godeye/managers/CameraManager.kt
deleted file mode 100644
index b2eb7e4..0000000
--- a/app/src/main/java/com/example/godeye/managers/CameraManager.kt
+++ /dev/null
@@ -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 = _isRecording.asStateFlow()
-
- private val _currentCameraType = MutableStateFlow(null)
- val currentCameraType: StateFlow = _currentCameraType.asStateFlow()
-
- private val _error = MutableStateFlow(null)
- val error: StateFlow = _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 {
- 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
- }
-}
diff --git a/app/src/main/java/com/example/godeye/managers/PermissionManager.kt b/app/src/main/java/com/example/godeye/managers/PermissionManager.kt
index 9fd5ca0..63141dd 100644
--- a/app/src/main/java/com/example/godeye/managers/PermissionManager.kt
+++ b/app/src/main/java/com/example/godeye/managers/PermissionManager.kt
@@ -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 = _permissionsGranted.asStateFlow()
+
+ private val _missingPermissions = MutableStateFlow>(emptyList())
+ val missingPermissions: StateFlow> = _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 {
- return REQUIRED_PERMISSIONS.filter { permission ->
- ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
- }
- }
-
- /**
- * Получить список отсутствующих разрешений для камеры
- */
- fun getMissingCameraPermissions(): List {
- 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()
+
+ 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 {
+ return REQUIRED_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
+ }
+
+ /**
+ * Получение списка критически важных отсутствующих разрешений
+ */
+ fun getMissingCriticalPermissions(): Array {
+ return CRITICAL_PERMISSIONS.filter { !isPermissionGranted(it) }.toTypedArray()
+ }
+
+ /**
+ * Обработка результата запроса разрешений
+ */
+ fun onPermissionsResult(
+ permissions: Array,
+ grantResults: IntArray
+ ): PermissionResult {
+ Logger.step("PERMISSION_RESULT", "Processing permission request result")
+
+ val granted = mutableListOf()
+ val denied = mutableListOf()
+
+ 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, val denied: List) : PermissionResult()
+ data class CriticalDenied(val denied: List) : PermissionResult()
+}
diff --git a/app/src/main/java/com/example/godeye/managers/SessionManager.kt b/app/src/main/java/com/example/godeye/managers/SessionManager.kt
index ed5e776..7687eff 100644
--- a/app/src/main/java/com/example/godeye/managers/SessionManager.kt
+++ b/app/src/main/java/com/example/godeye/managers/SessionManager.kt
@@ -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>(emptyList())
- val activeSessions: StateFlow> = _activeSessions.asStateFlow()
+ private val activeSessions = ConcurrentHashMap()
+
+ private val _sessions = MutableStateFlow>(emptyMap())
+ val sessions: StateFlow> = _sessions.asStateFlow()
+
+ private val _activeSessionCount = MutableStateFlow(0)
+ val activeSessionCount: StateFlow = _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 {
+ 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
)
diff --git a/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt b/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt
deleted file mode 100644
index 34eb98a..0000000
--- a/app/src/main/java/com/example/godeye/managers/WebRTCManager.kt
+++ /dev/null
@@ -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 = _connectionState.asStateFlow()
-
- private val _error = MutableStateFlow(null)
- val error: StateFlow = _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
- }
-}
diff --git a/app/src/main/java/com/example/godeye/models/CameraModels.kt b/app/src/main/java/com/example/godeye/models/CameraModels.kt
new file mode 100644
index 0000000..3a4620e
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/models/CameraModels.kt
@@ -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, // Поддерживаемые разрешения
+ val focalLengths: List // Фокусные расстояния для определения типа
+)
diff --git a/app/src/main/java/com/example/godeye/models/Models.kt b/app/src/main/java/com/example/godeye/models/Models.kt
index caa27f1..dd66b60 100644
--- a/app/src/main/java/com/example/godeye/models/Models.kt
+++ b/app/src/main/java/com/example/godeye/models/Models.kt
@@ -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
-)
-
-/**
- * Активная сессия с оператором
- */
-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 = 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"
+}
diff --git a/app/src/main/java/com/example/godeye/models/SocketEvents.kt b/app/src/main/java/com/example/godeye/models/SocketEvents.kt
new file mode 100644
index 0000000..5d814d8
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/models/SocketEvents.kt
@@ -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
+)
+
+/**
+ * Активная сессия камеры с оператором
+ * Соответствует требованиям ТЗ для управления 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()
+}
diff --git a/app/src/main/java/com/example/godeye/services/CameraService.kt b/app/src/main/java/com/example/godeye/services/CameraService.kt
deleted file mode 100644
index 25b3ac1..0000000
--- a/app/src/main/java/com/example/godeye/services/CameraService.kt
+++ /dev/null
@@ -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 = _isActive.asStateFlow()
-
- private val _error = MutableStateFlow(null)
- val error: StateFlow = _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")
- }
-}
diff --git a/app/src/main/java/com/example/godeye/services/SocketService.kt b/app/src/main/java/com/example/godeye/services/SocketService.kt
index 1050e9d..76c7b4d 100644
--- a/app/src/main/java/com/example/godeye/services/SocketService.kt
+++ b/app/src/main/java/com/example/godeye/services/SocketService.kt
@@ -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.asStateFlow()
private val _deviceId = MutableStateFlow("")
val deviceId: StateFlow = _deviceId.asStateFlow()
- private val _error = MutableStateFlow(null)
- val error: StateFlow = _error.asStateFlow()
+ // События для передачи в UI
+ private val _cameraRequests = MutableStateFlow(null)
+ val cameraRequests: StateFlow = _cameraRequests.asStateFlow()
- // События для UI
- private val _cameraRequest = MutableStateFlow(null)
- val cameraRequest: StateFlow = _cameraRequest.asStateFlow()
-
- private val _webrtcOffer = MutableStateFlow(null)
- val webrtcOffer: StateFlow = _webrtcOffer.asStateFlow()
-
- private val _webrtcAnswer = MutableStateFlow(null)
- val webrtcAnswer: StateFlow = _webrtcAnswer.asStateFlow()
-
- private val _webrtcIceCandidate = MutableStateFlow(null)
- val webrtcIceCandidate: StateFlow = _webrtcIceCandidate.asStateFlow()
-
- private val _cameraSwitchRequest = MutableStateFlow?>(null) // sessionId, newCameraType
- val cameraSwitchRequest: StateFlow?> = _cameraSwitchRequest.asStateFlow()
-
- private val _sessionDisconnect = MutableStateFlow(null) // sessionId
- val sessionDisconnect: StateFlow = _sessionDisconnect.asStateFlow()
+ private val _webRTCEvents = MutableStateFlow(null)
+ val webRTCEvents: StateFlow = _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()
+}
diff --git a/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt
new file mode 100644
index 0000000..536f69c
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/streaming/HLSStreamManager.kt
@@ -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()
+ 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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt
new file mode 100644
index 0000000..ee533df
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/streaming/RTSPStreamManager.kt
@@ -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()
+
+ 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, 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
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt
new file mode 100644
index 0000000..481654f
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/streaming/UDPStreamManager.kt
@@ -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 {
+ 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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt b/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt
new file mode 100644
index 0000000..fbb7dbe
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/streaming/UnifiedStreamingManager.kt
@@ -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.asStateFlow()
+
+ private val _availableProtocols = MutableStateFlow>(emptyList())
+ val availableProtocols: StateFlow> = _availableProtocols.asStateFlow()
+
+ private val _activeStreams = MutableStateFlow>(emptyMap())
+ val activeStreams: StateFlow> = _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()
+
+ 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 = 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()
+
+ 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) {
+ 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 {
+ val stats = mutableMapOf()
+
+ 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
+)
diff --git a/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt b/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt
new file mode 100644
index 0000000..7c0cdc7
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/streaming/WebRTCStreamManager.kt
@@ -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 = _connectionState.asStateFlow()
+
+ private val _isStreaming = MutableStateFlow(false)
+ val isStreaming: StateFlow = _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) {}
+ 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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt b/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt
new file mode 100644
index 0000000..d1aef4b
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/ui/components/AnimatedComponents.kt
@@ -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
+}
diff --git a/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt b/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt
deleted file mode 100644
index 3d3ab11..0000000
--- a/app/src/main/java/com/example/godeye/ui/components/CameraRequestDialog.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt b/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt
deleted file mode 100644
index c4147ca..0000000
--- a/app/src/main/java/com/example/godeye/ui/components/ConnectionStatusCard.kt
+++ /dev/null
@@ -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 {
- 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)
- )
- }
-}
diff --git a/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt b/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt
new file mode 100644
index 0000000..7f89f05
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/ui/components/MainScreenComponents.kt
@@ -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,
+ 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,
+ 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("Запустить")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt b/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt
deleted file mode 100644
index 6b0a0f9..0000000
--- a/app/src/main/java/com/example/godeye/ui/components/SessionsList.kt
+++ /dev/null
@@ -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,
- 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
- }
-}
diff --git a/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt b/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt
new file mode 100644
index 0000000..c0f64f9
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/ui/components/SettingsScreen.kt
@@ -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
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt b/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt
new file mode 100644
index 0000000..d0c02a9
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/ui/dialogs/CameraRequestDialog.kt
@@ -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)"
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt b/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt
deleted file mode 100644
index e872d7f..0000000
--- a/app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt
+++ /dev/null
@@ -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,
- 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
- )
- }
- }
-}
diff --git a/app/src/main/java/com/example/godeye/ui/theme/Color.kt b/app/src/main/java/com/example/godeye/ui/theme/Color.kt
deleted file mode 100644
index 55ca619..0000000
--- a/app/src/main/java/com/example/godeye/ui/theme/Color.kt
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt b/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
new file mode 100644
index 0000000..6ea6862
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/ui/theme/GodEyeTheme.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/com/example/godeye/ui/theme/Theme.kt b/app/src/main/java/com/example/godeye/ui/theme/Theme.kt
deleted file mode 100644
index 2fae9d9..0000000
--- a/app/src/main/java/com/example/godeye/ui/theme/Theme.kt
+++ /dev/null
@@ -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
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/godeye/ui/theme/Type.kt b/app/src/main/java/com/example/godeye/ui/theme/Type.kt
deleted file mode 100644
index a2e1fa7..0000000
--- a/app/src/main/java/com/example/godeye/ui/theme/Type.kt
+++ /dev/null
@@ -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
- )
- */
-)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt b/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt
deleted file mode 100644
index 83a46ee..0000000
--- a/app/src/main/java/com/example/godeye/ui/viewmodels/MainViewModel.kt
+++ /dev/null
@@ -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()
- 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 = _uiState.asStateFlow()
-
- // События для UI
- private val _events = MutableSharedFlow()
- val events: SharedFlow = _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 {
- 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()
-}
diff --git a/app/src/main/java/com/example/godeye/utils/Constants.kt b/app/src/main/java/com/example/godeye/utils/Constants.kt
index ff58c7a..41dd1e4 100644
--- a/app/src/main/java/com/example/godeye/utils/Constants.kt
+++ b/app/src/main/java/com/example/godeye/utils/Constants.kt
@@ -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
+ }
}
diff --git a/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt b/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
new file mode 100644
index 0000000..62b7334
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/utils/ErrorHandler.kt
@@ -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()
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/utils/Extensions.kt b/app/src/main/java/com/example/godeye/utils/Extensions.kt
index 40efb95..0fdcdf2 100644
--- a/app/src/main/java/com/example/godeye/utils/Extensions.kt
+++ b/app/src/main/java/com/example/godeye/utils/Extensions.kt
@@ -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 {
- val cameras = mutableListOf()
- 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 Flow.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)}"
}
diff --git a/app/src/main/java/com/example/godeye/utils/Logger.kt b/app/src/main/java/com/example/godeye/utils/Logger.kt
new file mode 100644
index 0000000..a289253
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/utils/Logger.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt b/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt
new file mode 100644
index 0000000..a04dd54
--- /dev/null
+++ b/app/src/main/java/com/example/godeye/webrtc/WebRTCManager.kt
@@ -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()
+ private var localVideoTrack: VideoTrack? = null
+ private var localAudioTrack: AudioTrack? = null
+ private var videoCapturer: CameraVideoCapturer? = null
+
+ // Состояния соединения
+ private val _connectionState = MutableStateFlow>(emptyMap())
+ val connectionState: StateFlow> = _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) {
+ 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()
+
+ 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) {}
+}
diff --git a/app/src/main/res/drawable/circle_button_background.xml b/app/src/main/res/drawable/circle_button_background.xml
new file mode 100644
index 0000000..de89ca7
--- /dev/null
+++ b/app/src/main/res/drawable/circle_button_background.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_legacy_camera.xml b/app/src/main/res/layout/activity_legacy_camera.xml
new file mode 100644
index 0000000..55033a0
--- /dev/null
+++ b/app/src/main/res/layout/activity_legacy_camera.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_legacy_main.xml b/app/src/main/res/layout/activity_legacy_main.xml
new file mode 100644
index 0000000..1f76093
--- /dev/null
+++ b/app/src/main/res/layout/activity_legacy_main.xml
@@ -0,0 +1,247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 33c0c97..b5516c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,80 +1,19 @@
+
GodEye Signal Center
-
-
- ID устройства:
- URL сервера:
- http://192.168.1.100:3001
- Статус подключения:
- Подключиться
- Отключиться
- Активные сессии:
- Нет активных сессий
-
-
- Отключено
- Подключение...
- Подключено
- Ошибка подключения
- Переподключение...
-
-
+ Статус подключения
+ Подключиться
+ Отключиться
+ URL сервера
+ Информация об устройстве
Запрос доступа к камере
- Оператор %1$s запрашивает доступ к камере %2$s
- ID сессии: %1$s
- Разрешить
- Отклонить
- Запомнить для этого оператора
+ Разрешить
+ Отклонить
+ Активные сессии
+ Нет активных сессий
+ Ожидание запросов операторов
+ Устройство готово к приему сессий
+ Требуются разрешения
+ Предоставить разрешения
+
-
- Основная
- Фронтальная
- Широкоугольная
- Телеобъектив
-
-
- Оператор:
- Камера:
- Длительность:
- WebRTC: %1$s
- Подключено
- Отключено
- Завершить
-
-
- Ошибка сети
- Нет разрешения на камеру
- Нет разрешения на микрофон
- Камера недоступна
- Ошибка WebRTC соединения
- Ошибка WebSocket: %1$s
- Ошибка камеры: %1$s
- Неизвестная ошибка
-
-
- Необходимы разрешения
- Для работы приложения необходимы разрешения на камеру, микрофон и уведомления
- Предоставить разрешения
- Без разрешений приложение не может работать
-
-
- GodEye Signal Center
- Подключено к серверу
- Отключено от сервера
- GodEye Camera
- Активных сессий: %1$d
- Камера готова к работе
-
-
- OK
- Отмена
- Настройки
- Загрузка...
- Повторить
-
-
- %1$02d:%2$02d:%3$02d
- сек
- мин
- ч
-
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 5d63c43..31c73a8 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,7 @@
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..254aa7c
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,14 @@
+
+
+
+ 192.168.219.108
+ 10.0.2.2
+ localhost
+
+
+
+
+
+
+
+
diff --git a/install_to_lg_g6.sh b/install_to_lg_g6.sh
new file mode 100755
index 0000000..8b14707
--- /dev/null
+++ b/install_to_lg_g6.sh
@@ -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
diff --git a/node_modules/.bin/mime b/node_modules/.bin/mime
deleted file mode 120000
index fbb7ee0..0000000
--- a/node_modules/.bin/mime
+++ /dev/null
@@ -1 +0,0 @@
-../mime/cli.js
\ No newline at end of file
diff --git a/node_modules/.bin/nodemon b/node_modules/.bin/nodemon
deleted file mode 120000
index 1056ddc..0000000
--- a/node_modules/.bin/nodemon
+++ /dev/null
@@ -1 +0,0 @@
-../nodemon/bin/nodemon.js
\ No newline at end of file
diff --git a/node_modules/.bin/nodetouch b/node_modules/.bin/nodetouch
deleted file mode 120000
index 3409fdb..0000000
--- a/node_modules/.bin/nodetouch
+++ /dev/null
@@ -1 +0,0 @@
-../touch/bin/nodetouch.js
\ No newline at end of file
diff --git a/node_modules/.bin/semver b/node_modules/.bin/semver
deleted file mode 120000
index 5aaadf4..0000000
--- a/node_modules/.bin/semver
+++ /dev/null
@@ -1 +0,0 @@
-../semver/bin/semver.js
\ No newline at end of file
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
deleted file mode 100644
index 9a82be0..0000000
--- a/node_modules/.package-lock.json
+++ /dev/null
@@ -1,1316 +0,0 @@
-{
- "name": "godeye-test-server",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "node_modules/@socket.io/component-emitter": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
- "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
- },
- "node_modules/@types/cors": {
- "version": "2.8.19",
- "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
- "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/node": {
- "version": "24.5.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
- "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
- "dependencies": {
- "undici-types": "~7.12.0"
- }
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
- },
- "node_modules/base64id": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
- "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
- "engines": {
- "node": "^4.5.0 || >= 5.9"
- }
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
- "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.13.0",
- "raw-body": "2.5.2",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
- },
- "node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
- "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
- },
- "node_modules/cors": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
- "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/engine.io": {
- "version": "6.6.4",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
- "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
- "dependencies": {
- "@types/cors": "^2.8.12",
- "@types/node": ">=10.0.0",
- "accepts": "~1.3.4",
- "base64id": "2.0.0",
- "cookie": "~0.7.2",
- "cors": "~2.8.5",
- "debug": "~4.3.1",
- "engine.io-parser": "~5.2.1",
- "ws": "~8.17.1"
- },
- "engines": {
- "node": ">=10.2.0"
- }
- },
- "node_modules/engine.io-parser": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
- "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/engine.io/node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/engine.io/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/engine.io/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/express": {
- "version": "4.21.2",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
- "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.3",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.7.1",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.3.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.12",
- "proxy-addr": "~2.0.7",
- "qs": "6.13.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.19.0",
- "serve-static": "1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
- "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "2.0.1",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ignore-by-default": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
- "dev": true
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/nodemon": {
- "version": "3.1.10",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
- "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
- "dev": true,
- "dependencies": {
- "chokidar": "^3.5.2",
- "debug": "^4",
- "ignore-by-default": "^1.0.1",
- "minimatch": "^3.1.2",
- "pstree.remy": "^1.1.8",
- "semver": "^7.5.3",
- "simple-update-notifier": "^2.0.0",
- "supports-color": "^5.5.0",
- "touch": "^3.1.0",
- "undefsafe": "^2.0.5"
- },
- "bin": {
- "nodemon": "bin/nodemon.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/nodemon"
- }
- },
- "node_modules/nodemon/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/nodemon/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/pstree.remy": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
- "dev": true
- },
- "node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
- "dependencies": {
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
- "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ]
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
- },
- "node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
- "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/serve-static": {
- "version": "1.16.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
- "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.19.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
- },
- "node_modules/side-channel": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3",
- "side-channel-list": "^1.0.0",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/simple-update-notifier": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
- "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
- "dev": true,
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/socket.io": {
- "version": "4.8.1",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
- "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
- "dependencies": {
- "accepts": "~1.3.4",
- "base64id": "~2.0.0",
- "cors": "~2.8.5",
- "debug": "~4.3.2",
- "engine.io": "~6.6.0",
- "socket.io-adapter": "~2.5.2",
- "socket.io-parser": "~4.2.4"
- },
- "engines": {
- "node": ">=10.2.0"
- }
- },
- "node_modules/socket.io-adapter": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
- "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
- "dependencies": {
- "debug": "~4.3.4",
- "ws": "~8.17.1"
- }
- },
- "node_modules/socket.io-adapter/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/socket.io-adapter/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/socket.io-parser": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
- "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
- "dependencies": {
- "@socket.io/component-emitter": "~3.1.0",
- "debug": "~4.3.1"
- },
- "engines": {
- "node": ">=10.0.0"
- }
- },
- "node_modules/socket.io-parser/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/socket.io-parser/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/socket.io/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/socket.io/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/touch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
- "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
- "dev": true,
- "bin": {
- "nodetouch": "bin/nodetouch.js"
- }
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/undefsafe": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
- "dev": true
- },
- "node_modules/undici-types": {
- "version": "7.12.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
- "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/ws": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
- "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- }
- }
-}
diff --git a/node_modules/@socket.io/component-emitter/LICENSE b/node_modules/@socket.io/component-emitter/LICENSE
deleted file mode 100644
index de51692..0000000
--- a/node_modules/@socket.io/component-emitter/LICENSE
+++ /dev/null
@@ -1,24 +0,0 @@
-(The MIT License)
-
-Copyright (c) 2014 Component contributors
-
-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.
diff --git a/node_modules/@socket.io/component-emitter/Readme.md b/node_modules/@socket.io/component-emitter/Readme.md
deleted file mode 100644
index feb36f1..0000000
--- a/node_modules/@socket.io/component-emitter/Readme.md
+++ /dev/null
@@ -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
diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts b/node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
deleted file mode 100644
index 49a74e1..0000000
--- a/node_modules/@socket.io/component-emitter/lib/cjs/index.d.ts
+++ /dev/null
@@ -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 = keyof Map & (string | symbol);
-
-/** The tuple type representing the parameters of an event listener */
-export type EventParams<
- Map extends EventsMap,
- Ev extends EventNames
- > = Parameters;
-
-/**
- * The event names that are either in ReservedEvents or in UserEvents
- */
-export type ReservedOrUserEventNames<
- ReservedEventsMap extends EventsMap,
- UserEvents extends EventsMap
- > = EventNames | EventNames;
-
-/**
- * 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
- > = FallbackToUntypedListener<
- Ev extends EventNames
- ? ReservedEvents[Ev]
- : Ev extends EventNames
- ? 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] extends [never]
- ? (...args: any[]) => void | Promise
- : 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: Ev,
- listener: ReservedOrUserListener
- ): 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: Ev,
- listener: ReservedOrUserListener
- ): this;
-
- /**
- * Removes the `listener` function as an event listener for `ev`.
- *
- * @param ev Name of the event
- * @param listener Callback function
- */
- off>(
- ev?: Ev,
- listener?: ReservedOrUserListener
- ): this;
-
- /**
- * Emits an event.
- *
- * @param ev Name of the event
- * @param args Values to send to listeners of this event
- */
- emit>(
- ev: Ev,
- ...args: EventParams
- ): 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: Ev,
- ...args: EventParams
- ): this;
-
- /**
- * Returns the listeners listening to an event.
- *
- * @param event Event name
- * @returns Array of listeners subscribed to `event`
- */
- listeners>(
- event: Ev
- ): ReservedOrUserListener[];
-
- /**
- * Returns true if there is a listener for this event.
- *
- * @param event Event name
- * @returns boolean
- */
- hasListeners<
- Ev extends ReservedOrUserEventNames
- >(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
- >(
- ev?: Ev,
- listener?: ReservedOrUserListener
- ): this;
-
- /**
- * Removes all `listener` function as an event listener for `ev`.
- *
- * @param ev Name of the event
- */
- removeAllListeners<
- Ev extends ReservedOrUserEventNames
- >(ev?: Ev): this;
-}
diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/index.js b/node_modules/@socket.io/component-emitter/lib/cjs/index.js
deleted file mode 100644
index e0d5497..0000000
--- a/node_modules/@socket.io/component-emitter/lib/cjs/index.js
+++ /dev/null
@@ -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;
-};
diff --git a/node_modules/@socket.io/component-emitter/lib/cjs/package.json b/node_modules/@socket.io/component-emitter/lib/cjs/package.json
deleted file mode 100644
index b6cc1b6..0000000
--- a/node_modules/@socket.io/component-emitter/lib/cjs/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "@socket.io/component-emitter",
- "type": "commonjs"
-}
diff --git a/node_modules/@socket.io/component-emitter/lib/esm/index.d.ts b/node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
deleted file mode 100644
index 49a74e1..0000000
--- a/node_modules/@socket.io/component-emitter/lib/esm/index.d.ts
+++ /dev/null
@@ -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 = keyof Map & (string | symbol);
-
-/** The tuple type representing the parameters of an event listener */
-export type EventParams<
- Map extends EventsMap,
- Ev extends EventNames
- > = Parameters;
-
-/**
- * The event names that are either in ReservedEvents or in UserEvents
- */
-export type ReservedOrUserEventNames<
- ReservedEventsMap extends EventsMap,
- UserEvents extends EventsMap
- > = EventNames | EventNames;
-
-/**
- * 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
- > = FallbackToUntypedListener<
- Ev extends EventNames
- ? ReservedEvents[Ev]
- : Ev extends EventNames
- ? 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] extends [never]
- ? (...args: any[]) => void | Promise
- : 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: Ev,
- listener: ReservedOrUserListener
- ): 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: Ev,
- listener: ReservedOrUserListener
- ): this;
-
- /**
- * Removes the `listener` function as an event listener for `ev`.
- *
- * @param ev Name of the event
- * @param listener Callback function
- */
- off>(
- ev?: Ev,
- listener?: ReservedOrUserListener
- ): this;
-
- /**
- * Emits an event.
- *
- * @param ev Name of the event
- * @param args Values to send to listeners of this event
- */
- emit>(
- ev: Ev,
- ...args: EventParams
- ): 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: Ev,
- ...args: EventParams
- ): this;
-
- /**
- * Returns the listeners listening to an event.
- *
- * @param event Event name
- * @returns Array of listeners subscribed to `event`
- */
- listeners>(
- event: Ev
- ): ReservedOrUserListener[];
-
- /**
- * Returns true if there is a listener for this event.
- *
- * @param event Event name
- * @returns boolean
- */
- hasListeners<
- Ev extends ReservedOrUserEventNames
- >(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
- >(
- ev?: Ev,
- listener?: ReservedOrUserListener
- ): this;
-
- /**
- * Removes all `listener` function as an event listener for `ev`.
- *
- * @param ev Name of the event
- */
- removeAllListeners<
- Ev extends ReservedOrUserEventNames
- >(ev?: Ev): this;
-}
diff --git a/node_modules/@socket.io/component-emitter/lib/esm/index.js b/node_modules/@socket.io/component-emitter/lib/esm/index.js
deleted file mode 100644
index b2e5c3f..0000000
--- a/node_modules/@socket.io/component-emitter/lib/esm/index.js
+++ /dev/null
@@ -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;
-};
diff --git a/node_modules/@socket.io/component-emitter/lib/esm/package.json b/node_modules/@socket.io/component-emitter/lib/esm/package.json
deleted file mode 100644
index 0511a0c..0000000
--- a/node_modules/@socket.io/component-emitter/lib/esm/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "@socket.io/component-emitter",
- "type": "module"
-}
diff --git a/node_modules/@socket.io/component-emitter/package.json b/node_modules/@socket.io/component-emitter/package.json
deleted file mode 100644
index 3a18a8d..0000000
--- a/node_modules/@socket.io/component-emitter/package.json
+++ /dev/null
@@ -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/"
- ]
-}
diff --git a/node_modules/@types/cors/LICENSE b/node_modules/@types/cors/LICENSE
deleted file mode 100644
index 9e841e7..0000000
--- a/node_modules/@types/cors/LICENSE
+++ /dev/null
@@ -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
diff --git a/node_modules/@types/cors/README.md b/node_modules/@types/cors/README.md
deleted file mode 100644
index b1834a3..0000000
--- a/node_modules/@types/cors/README.md
+++ /dev/null
@@ -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
-///
-
-import { IncomingHttpHeaders } from "http";
-
-type StaticOrigin = boolean | string | RegExp | Array;
-
-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 = (
- req: T,
- callback: (err: Error | null, options?: CorsOptions) => void,
- ) => void;
-}
-
-declare function e(
- options?: e.CorsOptions | e.CorsOptionsDelegate,
-): (
- 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).
diff --git a/node_modules/@types/cors/index.d.ts b/node_modules/@types/cors/index.d.ts
deleted file mode 100644
index 5ab0dcf..0000000
--- a/node_modules/@types/cors/index.d.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-///
-
-import { IncomingHttpHeaders } from "http";
-
-type StaticOrigin = boolean | string | RegExp | Array;
-
-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 = (
- req: T,
- callback: (err: Error | null, options?: CorsOptions) => void,
- ) => void;
-}
-
-declare function e(
- options?: e.CorsOptions | e.CorsOptionsDelegate,
-): (
- req: T,
- res: {
- statusCode?: number | undefined;
- setHeader(key: string, value: string): any;
- end(): any;
- },
- next: (err?: any) => any,
-) => void;
-export = e;
diff --git a/node_modules/@types/cors/package.json b/node_modules/@types/cors/package.json
deleted file mode 100644
index 4824a17..0000000
--- a/node_modules/@types/cors/package.json
+++ /dev/null
@@ -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"
-}
\ No newline at end of file
diff --git a/node_modules/@types/node/LICENSE b/node_modules/@types/node/LICENSE
deleted file mode 100644
index 9e841e7..0000000
--- a/node_modules/@types/node/LICENSE
+++ /dev/null
@@ -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
diff --git a/node_modules/@types/node/README.md b/node_modules/@types/node/README.md
deleted file mode 100644
index 124961e..0000000
--- a/node_modules/@types/node/README.md
+++ /dev/null
@@ -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).
diff --git a/node_modules/@types/node/assert.d.ts b/node_modules/@types/node/assert.d.ts
deleted file mode 100644
index b79fc21..0000000
--- a/node_modules/@types/node/assert.d.ts
+++ /dev/null
@@ -1,1056 +0,0 @@
-/**
- * The `node:assert` module provides a set of assertion functions for verifying
- * invariants.
- * @see [source](https://github.com/nodejs/node/blob/v24.x/lib/assert.js)
- */
-declare module "assert" {
- /**
- * An alias of {@link ok}.
- * @since v0.5.9
- * @param value The input that is checked for being truthy.
- */
- function assert(value: unknown, message?: string | Error): asserts value;
- namespace assert {
- /**
- * Indicates the failure of an assertion. All errors thrown by the `node:assert` module will be instances of the `AssertionError` class.
- */
- class AssertionError extends Error {
- /**
- * Set to the `actual` argument for methods such as {@link assert.strictEqual()}.
- */
- actual: unknown;
- /**
- * Set to the `expected` argument for methods such as {@link assert.strictEqual()}.
- */
- expected: unknown;
- /**
- * Set to the passed in operator value.
- */
- operator: string;
- /**
- * Indicates if the message was auto-generated (`true`) or not.
- */
- generatedMessage: boolean;
- /**
- * Value is always `ERR_ASSERTION` to show that the error is an assertion error.
- */
- code: "ERR_ASSERTION";
- constructor(options?: {
- /** If provided, the error message is set to this value. */
- message?: string | undefined;
- /** The `actual` property on the error instance. */
- actual?: unknown | undefined;
- /** The `expected` property on the error instance. */
- expected?: unknown | undefined;
- /** The `operator` property on the error instance. */
- operator?: string | undefined;
- /** If provided, the generated stack trace omits frames before this function. */
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
- stackStartFn?: Function | undefined;
- });
- }
- /**
- * This feature is deprecated and will be removed in a future version.
- * Please consider using alternatives such as the `mock` helper function.
- * @since v14.2.0, v12.19.0
- * @deprecated Deprecated
- */
- class CallTracker {
- /**
- * The wrapper function is expected to be called exactly `exact` times. If the
- * function has not been called exactly `exact` times when `tracker.verify()` is called, then `tracker.verify()` will throw an
- * error.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * // Creates call tracker.
- * const tracker = new assert.CallTracker();
- *
- * function func() {}
- *
- * // Returns a function that wraps func() that must be called exact times
- * // before tracker.verify().
- * const callsfunc = tracker.calls(func);
- * ```
- * @since v14.2.0, v12.19.0
- * @param [fn='A no-op function']
- * @param [exact=1]
- * @return A function that wraps `fn`.
- */
- calls(exact?: number): () => void;
- calls(fn: undefined, exact?: number): () => void;
- calls any>(fn: Func, exact?: number): Func;
- calls any>(fn?: Func, exact?: number): Func | (() => void);
- /**
- * Example:
- *
- * ```js
- * import assert from 'node:assert';
- *
- * const tracker = new assert.CallTracker();
- *
- * function func() {}
- * const callsfunc = tracker.calls(func);
- * callsfunc(1, 2, 3);
- *
- * assert.deepStrictEqual(tracker.getCalls(callsfunc),
- * [{ thisArg: undefined, arguments: [1, 2, 3] }]);
- * ```
- * @since v18.8.0, v16.18.0
- * @return An array with all the calls to a tracked function.
- */
- getCalls(fn: Function): CallTrackerCall[];
- /**
- * The arrays contains information about the expected and actual number of calls of
- * the functions that have not been called the expected number of times.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * // Creates call tracker.
- * const tracker = new assert.CallTracker();
- *
- * function func() {}
- *
- * // Returns a function that wraps func() that must be called exact times
- * // before tracker.verify().
- * const callsfunc = tracker.calls(func, 2);
- *
- * // Returns an array containing information on callsfunc()
- * console.log(tracker.report());
- * // [
- * // {
- * // message: 'Expected the func function to be executed 2 time(s) but was
- * // executed 0 time(s).',
- * // actual: 0,
- * // expected: 2,
- * // operator: 'func',
- * // stack: stack trace
- * // }
- * // ]
- * ```
- * @since v14.2.0, v12.19.0
- * @return An array of objects containing information about the wrapper functions returned by {@link tracker.calls()}.
- */
- report(): CallTrackerReportInformation[];
- /**
- * Reset calls of the call tracker. If a tracked function is passed as an argument, the calls will be reset for it.
- * If no arguments are passed, all tracked functions will be reset.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * const tracker = new assert.CallTracker();
- *
- * function func() {}
- * const callsfunc = tracker.calls(func);
- *
- * callsfunc();
- * // Tracker was called once
- * assert.strictEqual(tracker.getCalls(callsfunc).length, 1);
- *
- * tracker.reset(callsfunc);
- * assert.strictEqual(tracker.getCalls(callsfunc).length, 0);
- * ```
- * @since v18.8.0, v16.18.0
- * @param fn a tracked function to reset.
- */
- reset(fn?: Function): void;
- /**
- * Iterates through the list of functions passed to {@link tracker.calls()} and will throw an error for functions that
- * have not been called the expected number of times.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * // Creates call tracker.
- * const tracker = new assert.CallTracker();
- *
- * function func() {}
- *
- * // Returns a function that wraps func() that must be called exact times
- * // before tracker.verify().
- * const callsfunc = tracker.calls(func, 2);
- *
- * callsfunc();
- *
- * // Will throw an error since callsfunc() was only called once.
- * tracker.verify();
- * ```
- * @since v14.2.0, v12.19.0
- */
- verify(): void;
- }
- interface CallTrackerCall {
- thisArg: object;
- arguments: unknown[];
- }
- interface CallTrackerReportInformation {
- message: string;
- /** The actual number of times the function was called. */
- actual: number;
- /** The number of times the function was expected to be called. */
- expected: number;
- /** The name of the function that is wrapped. */
- operator: string;
- /** A stack trace of the function. */
- stack: object;
- }
- type AssertPredicate = RegExp | (new() => object) | ((thrown: unknown) => boolean) | object | Error;
- /**
- * Throws an `AssertionError` with the provided error message or a default
- * error message. If the `message` parameter is an instance of an `Error` then
- * it will be thrown instead of the `AssertionError`.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.fail();
- * // AssertionError [ERR_ASSERTION]: Failed
- *
- * assert.fail('boom');
- * // AssertionError [ERR_ASSERTION]: boom
- *
- * assert.fail(new TypeError('need array'));
- * // TypeError: need array
- * ```
- *
- * Using `assert.fail()` with more than two arguments is possible but deprecated.
- * See below for further details.
- * @since v0.1.21
- * @param [message='Failed']
- */
- function fail(message?: string | Error): never;
- /** @deprecated since v10.0.0 - use fail([message]) or other assert functions instead. */
- function fail(
- actual: unknown,
- expected: unknown,
- message?: string | Error,
- operator?: string,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
- stackStartFn?: Function,
- ): never;
- /**
- * Tests if `value` is truthy. It is equivalent to `assert.equal(!!value, true, message)`.
- *
- * If `value` is not truthy, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is `undefined`, a default
- * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`.
- * If no arguments are passed in at all `message` will be set to the string:`` 'No value argument passed to `assert.ok()`' ``.
- *
- * Be aware that in the `repl` the error message will be different to the one
- * thrown in a file! See below for further details.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.ok(true);
- * // OK
- * assert.ok(1);
- * // OK
- *
- * assert.ok();
- * // AssertionError: No value argument passed to `assert.ok()`
- *
- * assert.ok(false, 'it\'s false');
- * // AssertionError: it's false
- *
- * // In the repl:
- * assert.ok(typeof 123 === 'string');
- * // AssertionError: false == true
- *
- * // In a file (e.g. test.js):
- * assert.ok(typeof 123 === 'string');
- * // AssertionError: The expression evaluated to a falsy value:
- * //
- * // assert.ok(typeof 123 === 'string')
- *
- * assert.ok(false);
- * // AssertionError: The expression evaluated to a falsy value:
- * //
- * // assert.ok(false)
- *
- * assert.ok(0);
- * // AssertionError: The expression evaluated to a falsy value:
- * //
- * // assert.ok(0)
- * ```
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * // Using `assert()` works the same:
- * assert(0);
- * // AssertionError: The expression evaluated to a falsy value:
- * //
- * // assert(0)
- * ```
- * @since v0.1.21
- */
- function ok(value: unknown, message?: string | Error): asserts value;
- /**
- * **Strict assertion mode**
- *
- * An alias of {@link strictEqual}.
- *
- * **Legacy assertion mode**
- *
- * > Stability: 3 - Legacy: Use {@link strictEqual} instead.
- *
- * Tests shallow, coercive equality between the `actual` and `expected` parameters
- * using the [`==` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Equality). `NaN` is specially handled
- * and treated as being identical if both sides are `NaN`.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * assert.equal(1, 1);
- * // OK, 1 == 1
- * assert.equal(1, '1');
- * // OK, 1 == '1'
- * assert.equal(NaN, NaN);
- * // OK
- *
- * assert.equal(1, 2);
- * // AssertionError: 1 == 2
- * assert.equal({ a: { b: 1 } }, { a: { b: 1 } });
- * // AssertionError: { a: { b: 1 } } == { a: { b: 1 } }
- * ```
- *
- * If the values are not equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default
- * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`.
- * @since v0.1.21
- */
- function equal(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * **Strict assertion mode**
- *
- * An alias of {@link notStrictEqual}.
- *
- * **Legacy assertion mode**
- *
- * > Stability: 3 - Legacy: Use {@link notStrictEqual} instead.
- *
- * Tests shallow, coercive inequality with the [`!=` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Inequality). `NaN` is
- * specially handled and treated as being identical if both sides are `NaN`.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * assert.notEqual(1, 2);
- * // OK
- *
- * assert.notEqual(1, 1);
- * // AssertionError: 1 != 1
- *
- * assert.notEqual(1, '1');
- * // AssertionError: 1 != '1'
- * ```
- *
- * If the values are equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default error
- * message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown instead of the `AssertionError`.
- * @since v0.1.21
- */
- function notEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * **Strict assertion mode**
- *
- * An alias of {@link deepStrictEqual}.
- *
- * **Legacy assertion mode**
- *
- * > Stability: 3 - Legacy: Use {@link deepStrictEqual} instead.
- *
- * Tests for deep equality between the `actual` and `expected` parameters. Consider
- * using {@link deepStrictEqual} instead. {@link deepEqual} can have
- * surprising results.
- *
- * _Deep equality_ means that the enumerable "own" properties of child objects
- * are also recursively evaluated by the following rules.
- * @since v0.1.21
- */
- function deepEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * **Strict assertion mode**
- *
- * An alias of {@link notDeepStrictEqual}.
- *
- * **Legacy assertion mode**
- *
- * > Stability: 3 - Legacy: Use {@link notDeepStrictEqual} instead.
- *
- * Tests for any deep inequality. Opposite of {@link deepEqual}.
- *
- * ```js
- * import assert from 'node:assert';
- *
- * const obj1 = {
- * a: {
- * b: 1,
- * },
- * };
- * const obj2 = {
- * a: {
- * b: 2,
- * },
- * };
- * const obj3 = {
- * a: {
- * b: 1,
- * },
- * };
- * const obj4 = { __proto__: obj1 };
- *
- * assert.notDeepEqual(obj1, obj1);
- * // AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } }
- *
- * assert.notDeepEqual(obj1, obj2);
- * // OK
- *
- * assert.notDeepEqual(obj1, obj3);
- * // AssertionError: { a: { b: 1 } } notDeepEqual { a: { b: 1 } }
- *
- * assert.notDeepEqual(obj1, obj4);
- * // OK
- * ```
- *
- * If the values are deeply equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a default
- * error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown
- * instead of the `AssertionError`.
- * @since v0.1.21
- */
- function notDeepEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * Tests strict equality between the `actual` and `expected` parameters as
- * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.strictEqual(1, 2);
- * // AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
- * //
- * // 1 !== 2
- *
- * assert.strictEqual(1, 1);
- * // OK
- *
- * assert.strictEqual('Hello foobar', 'Hello World!');
- * // AssertionError [ERR_ASSERTION]: Expected inputs to be strictly equal:
- * // + actual - expected
- * //
- * // + 'Hello foobar'
- * // - 'Hello World!'
- * // ^
- *
- * const apples = 1;
- * const oranges = 2;
- * assert.strictEqual(apples, oranges, `apples ${apples} !== oranges ${oranges}`);
- * // AssertionError [ERR_ASSERTION]: apples 1 !== oranges 2
- *
- * assert.strictEqual(1, '1', new TypeError('Inputs are not identical'));
- * // TypeError: Inputs are not identical
- * ```
- *
- * If the values are not strictly equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a
- * default error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown
- * instead of the `AssertionError`.
- * @since v0.1.21
- */
- function strictEqual(actual: unknown, expected: T, message?: string | Error): asserts actual is T;
- /**
- * Tests strict inequality between the `actual` and `expected` parameters as
- * determined by [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is).
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.notStrictEqual(1, 2);
- * // OK
- *
- * assert.notStrictEqual(1, 1);
- * // AssertionError [ERR_ASSERTION]: Expected "actual" to be strictly unequal to:
- * //
- * // 1
- *
- * assert.notStrictEqual(1, '1');
- * // OK
- * ```
- *
- * If the values are strictly equal, an `AssertionError` is thrown with a `message` property set equal to the value of the `message` parameter. If the `message` parameter is undefined, a
- * default error message is assigned. If the `message` parameter is an instance of an `Error` then it will be thrown
- * instead of the `AssertionError`.
- * @since v0.1.21
- */
- function notStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * Tests for deep equality between the `actual` and `expected` parameters.
- * "Deep" equality means that the enumerable "own" properties of child objects
- * are recursively evaluated also by the following rules.
- * @since v1.2.0
- */
- function deepStrictEqual(actual: unknown, expected: T, message?: string | Error): asserts actual is T;
- /**
- * Tests for deep strict inequality. Opposite of {@link deepStrictEqual}.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.notDeepStrictEqual({ a: 1 }, { a: '1' });
- * // OK
- * ```
- *
- * If the values are deeply and strictly equal, an `AssertionError` is thrown
- * with a `message` property set equal to the value of the `message` parameter. If
- * the `message` parameter is undefined, a default error message is assigned. If
- * the `message` parameter is an instance of an `Error` then it will be thrown
- * instead of the `AssertionError`.
- * @since v1.2.0
- */
- function notDeepStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * Expects the function `fn` to throw an error.
- *
- * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),
- * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), a validation function,
- * a validation object where each property will be tested for strict deep equality,
- * or an instance of error where each property will be tested for strict deep
- * equality including the non-enumerable `message` and `name` properties. When
- * using an object, it is also possible to use a regular expression, when
- * validating against a string property. See below for examples.
- *
- * If specified, `message` will be appended to the message provided by the `AssertionError` if the `fn` call fails to throw or in case the error validation
- * fails.
- *
- * Custom validation object/error instance:
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * const err = new TypeError('Wrong value');
- * err.code = 404;
- * err.foo = 'bar';
- * err.info = {
- * nested: true,
- * baz: 'text',
- * };
- * err.reg = /abc/i;
- *
- * assert.throws(
- * () => {
- * throw err;
- * },
- * {
- * name: 'TypeError',
- * message: 'Wrong value',
- * info: {
- * nested: true,
- * baz: 'text',
- * },
- * // Only properties on the validation object will be tested for.
- * // Using nested objects requires all properties to be present. Otherwise
- * // the validation is going to fail.
- * },
- * );
- *
- * // Using regular expressions to validate error properties:
- * assert.throws(
- * () => {
- * throw err;
- * },
- * {
- * // The `name` and `message` properties are strings and using regular
- * // expressions on those will match against the string. If they fail, an
- * // error is thrown.
- * name: /^TypeError$/,
- * message: /Wrong/,
- * foo: 'bar',
- * info: {
- * nested: true,
- * // It is not possible to use regular expressions for nested properties!
- * baz: 'text',
- * },
- * // The `reg` property contains a regular expression and only if the
- * // validation object contains an identical regular expression, it is going
- * // to pass.
- * reg: /abc/i,
- * },
- * );
- *
- * // Fails due to the different `message` and `name` properties:
- * assert.throws(
- * () => {
- * const otherErr = new Error('Not found');
- * // Copy all enumerable properties from `err` to `otherErr`.
- * for (const [key, value] of Object.entries(err)) {
- * otherErr[key] = value;
- * }
- * throw otherErr;
- * },
- * // The error's `message` and `name` properties will also be checked when using
- * // an error as validation object.
- * err,
- * );
- * ```
- *
- * Validate instanceof using constructor:
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.throws(
- * () => {
- * throw new Error('Wrong value');
- * },
- * Error,
- * );
- * ```
- *
- * Validate error message using [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions):
- *
- * Using a regular expression runs `.toString` on the error object, and will
- * therefore also include the error name.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.throws(
- * () => {
- * throw new Error('Wrong value');
- * },
- * /^Error: Wrong value$/,
- * );
- * ```
- *
- * Custom error validation:
- *
- * The function must return `true` to indicate all internal validations passed.
- * It will otherwise fail with an `AssertionError`.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.throws(
- * () => {
- * throw new Error('Wrong value');
- * },
- * (err) => {
- * assert(err instanceof Error);
- * assert(/value/.test(err));
- * // Avoid returning anything from validation functions besides `true`.
- * // Otherwise, it's not clear what part of the validation failed. Instead,
- * // throw an error about the specific validation that failed (as done in this
- * // example) and add as much helpful debugging information to that error as
- * // possible.
- * return true;
- * },
- * 'unexpected error',
- * );
- * ```
- *
- * `error` cannot be a string. If a string is provided as the second
- * argument, then `error` is assumed to be omitted and the string will be used for `message` instead. This can lead to easy-to-miss mistakes. Using the same
- * message as the thrown error message is going to result in an `ERR_AMBIGUOUS_ARGUMENT` error. Please read the example below carefully if using
- * a string as the second argument gets considered:
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * function throwingFirst() {
- * throw new Error('First');
- * }
- *
- * function throwingSecond() {
- * throw new Error('Second');
- * }
- *
- * function notThrowing() {}
- *
- * // The second argument is a string and the input function threw an Error.
- * // The first case will not throw as it does not match for the error message
- * // thrown by the input function!
- * assert.throws(throwingFirst, 'Second');
- * // In the next example the message has no benefit over the message from the
- * // error and since it is not clear if the user intended to actually match
- * // against the error message, Node.js throws an `ERR_AMBIGUOUS_ARGUMENT` error.
- * assert.throws(throwingSecond, 'Second');
- * // TypeError [ERR_AMBIGUOUS_ARGUMENT]
- *
- * // The string is only used (as message) in case the function does not throw:
- * assert.throws(notThrowing, 'Second');
- * // AssertionError [ERR_ASSERTION]: Missing expected exception: Second
- *
- * // If it was intended to match for the error message do this instead:
- * // It does not throw because the error messages match.
- * assert.throws(throwingSecond, /Second$/);
- *
- * // If the error message does not match, an AssertionError is thrown.
- * assert.throws(throwingFirst, /Second$/);
- * // AssertionError [ERR_ASSERTION]
- * ```
- *
- * Due to the confusing error-prone notation, avoid a string as the second
- * argument.
- * @since v0.1.21
- */
- function throws(block: () => unknown, message?: string | Error): void;
- function throws(block: () => unknown, error: AssertPredicate, message?: string | Error): void;
- /**
- * Asserts that the function `fn` does not throw an error.
- *
- * Using `assert.doesNotThrow()` is actually not useful because there
- * is no benefit in catching an error and then rethrowing it. Instead, consider
- * adding a comment next to the specific code path that should not throw and keep
- * error messages as expressive as possible.
- *
- * When `assert.doesNotThrow()` is called, it will immediately call the `fn` function.
- *
- * If an error is thrown and it is the same type as that specified by the `error` parameter, then an `AssertionError` is thrown. If the error is of a
- * different type, or if the `error` parameter is undefined, the error is
- * propagated back to the caller.
- *
- * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),
- * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), or a validation
- * function. See {@link throws} for more details.
- *
- * The following, for instance, will throw the `TypeError` because there is no
- * matching error type in the assertion:
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.doesNotThrow(
- * () => {
- * throw new TypeError('Wrong value');
- * },
- * SyntaxError,
- * );
- * ```
- *
- * However, the following will result in an `AssertionError` with the message
- * 'Got unwanted exception...':
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.doesNotThrow(
- * () => {
- * throw new TypeError('Wrong value');
- * },
- * TypeError,
- * );
- * ```
- *
- * If an `AssertionError` is thrown and a value is provided for the `message` parameter, the value of `message` will be appended to the `AssertionError` message:
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.doesNotThrow(
- * () => {
- * throw new TypeError('Wrong value');
- * },
- * /Wrong value/,
- * 'Whoops',
- * );
- * // Throws: AssertionError: Got unwanted exception: Whoops
- * ```
- * @since v0.1.21
- */
- function doesNotThrow(block: () => unknown, message?: string | Error): void;
- function doesNotThrow(block: () => unknown, error: AssertPredicate, message?: string | Error): void;
- /**
- * Throws `value` if `value` is not `undefined` or `null`. This is useful when
- * testing the `error` argument in callbacks. The stack trace contains all frames
- * from the error passed to `ifError()` including the potential new frames for `ifError()` itself.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.ifError(null);
- * // OK
- * assert.ifError(0);
- * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0
- * assert.ifError('error');
- * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error'
- * assert.ifError(new Error());
- * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error
- *
- * // Create some random error frames.
- * let err;
- * (function errorFrame() {
- * err = new Error('test error');
- * })();
- *
- * (function ifErrorFrame() {
- * assert.ifError(err);
- * })();
- * // AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error
- * // at ifErrorFrame
- * // at errorFrame
- * ```
- * @since v0.1.97
- */
- function ifError(value: unknown): asserts value is null | undefined;
- /**
- * Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately
- * calls the function and awaits the returned promise to complete. It will then
- * check that the promise is rejected.
- *
- * If `asyncFn` is a function and it throws an error synchronously, `assert.rejects()` will return a rejected `Promise` with that error. If the
- * function does not return a promise, `assert.rejects()` will return a rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v24.x/api/errors.html#err_invalid_return_value)
- * error. In both cases the error handler is skipped.
- *
- * Besides the async nature to await the completion behaves identically to {@link throws}.
- *
- * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),
- * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), a validation function,
- * an object where each property will be tested for, or an instance of error where
- * each property will be tested for including the non-enumerable `message` and `name` properties.
- *
- * If specified, `message` will be the message provided by the `{@link AssertionError}` if the `asyncFn` fails to reject.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * await assert.rejects(
- * async () => {
- * throw new TypeError('Wrong value');
- * },
- * {
- * name: 'TypeError',
- * message: 'Wrong value',
- * },
- * );
- * ```
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * await assert.rejects(
- * async () => {
- * throw new TypeError('Wrong value');
- * },
- * (err) => {
- * assert.strictEqual(err.name, 'TypeError');
- * assert.strictEqual(err.message, 'Wrong value');
- * return true;
- * },
- * );
- * ```
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.rejects(
- * Promise.reject(new Error('Wrong value')),
- * Error,
- * ).then(() => {
- * // ...
- * });
- * ```
- *
- * `error` cannot be a string. If a string is provided as the second argument, then `error` is assumed to
- * be omitted and the string will be used for `message` instead. This can lead to easy-to-miss mistakes. Please read the
- * example in {@link throws} carefully if using a string as the second argument gets considered.
- * @since v10.0.0
- */
- function rejects(block: (() => Promise) | Promise, message?: string | Error): Promise;
- function rejects(
- block: (() => Promise) | Promise,
- error: AssertPredicate,
- message?: string | Error,
- ): Promise;
- /**
- * Awaits the `asyncFn` promise or, if `asyncFn` is a function, immediately
- * calls the function and awaits the returned promise to complete. It will then
- * check that the promise is not rejected.
- *
- * If `asyncFn` is a function and it throws an error synchronously, `assert.doesNotReject()` will return a rejected `Promise` with that error. If
- * the function does not return a promise, `assert.doesNotReject()` will return a
- * rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v24.x/api/errors.html#err_invalid_return_value) error. In both cases
- * the error handler is skipped.
- *
- * Using `assert.doesNotReject()` is actually not useful because there is little
- * benefit in catching a rejection and then rejecting it again. Instead, consider
- * adding a comment next to the specific code path that should not reject and keep
- * error messages as expressive as possible.
- *
- * If specified, `error` can be a [`Class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes),
- * [`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), or a validation
- * function. See {@link throws} for more details.
- *
- * Besides the async nature to await the completion behaves identically to {@link doesNotThrow}.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * await assert.doesNotReject(
- * async () => {
- * throw new TypeError('Wrong value');
- * },
- * SyntaxError,
- * );
- * ```
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.doesNotReject(Promise.reject(new TypeError('Wrong value')))
- * .then(() => {
- * // ...
- * });
- * ```
- * @since v10.0.0
- */
- function doesNotReject(
- block: (() => Promise) | Promise,
- message?: string | Error,
- ): Promise;
- function doesNotReject(
- block: (() => Promise) | Promise,
- error: AssertPredicate,
- message?: string | Error,
- ): Promise;
- /**
- * Expects the `string` input to match the regular expression.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.match('I will fail', /pass/);
- * // AssertionError [ERR_ASSERTION]: The input did not match the regular ...
- *
- * assert.match(123, /pass/);
- * // AssertionError [ERR_ASSERTION]: The "string" argument must be of type string.
- *
- * assert.match('I will pass', /pass/);
- * // OK
- * ```
- *
- * If the values do not match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal
- * to the value of the `message` parameter. If the `message` parameter is
- * undefined, a default error message is assigned. If the `message` parameter is an
- * instance of an [Error](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
- * @since v13.6.0, v12.16.0
- */
- function match(value: string, regExp: RegExp, message?: string | Error): void;
- /**
- * Expects the `string` input not to match the regular expression.
- *
- * ```js
- * import assert from 'node:assert/strict';
- *
- * assert.doesNotMatch('I will fail', /fail/);
- * // AssertionError [ERR_ASSERTION]: The input was expected to not match the ...
- *
- * assert.doesNotMatch(123, /pass/);
- * // AssertionError [ERR_ASSERTION]: The "string" argument must be of type string.
- *
- * assert.doesNotMatch('I will pass', /different/);
- * // OK
- * ```
- *
- * If the values do match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal
- * to the value of the `message` parameter. If the `message` parameter is
- * undefined, a default error message is assigned. If the `message` parameter is an
- * instance of an [Error](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
- * @since v13.6.0, v12.16.0
- */
- function doesNotMatch(value: string, regExp: RegExp, message?: string | Error): void;
- /**
- * Tests for partial deep equality between the `actual` and `expected` parameters.
- * "Deep" equality means that the enumerable "own" properties of child objects
- * are recursively evaluated also by the following rules. "Partial" equality means
- * that only properties that exist on the `expected` parameter are going to be
- * compared.
- *
- * This method always passes the same test cases as `assert.deepStrictEqual()`,
- * behaving as a super set of it.
- * @since v22.13.0
- */
- function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void;
- /**
- * In strict assertion mode, non-strict methods behave like their corresponding strict methods. For example,
- * {@link deepEqual} will behave like {@link deepStrictEqual}.
- *
- * In strict assertion mode, error messages for objects display a diff. In legacy assertion mode, error
- * messages for objects display the objects, often truncated.
- *
- * To use strict assertion mode:
- *
- * ```js
- * import { strict as assert } from 'node:assert';
- * import assert from 'node:assert/strict';
- * ```
- *
- * Example error diff:
- *
- * ```js
- * import { strict as assert } from 'node:assert';
- *
- * assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
- * // AssertionError: Expected inputs to be strictly deep-equal:
- * // + actual - expected ... Lines skipped
- * //
- * // [
- * // [
- * // ...
- * // 2,
- * // + 3
- * // - '3'
- * // ],
- * // ...
- * // 5
- * // ]
- * ```
- *
- * To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS` environment variables. This will also
- * deactivate the colors in the REPL. For more on color support in terminal environments, read the tty
- * `getColorDepth()` documentation.
- *
- * @since v15.0.0, v13.9.0, v12.16.2, v9.9.0
- */
- namespace strict {
- type AssertionError = assert.AssertionError;
- type AssertPredicate = assert.AssertPredicate;
- type CallTrackerCall = assert.CallTrackerCall;
- type CallTrackerReportInformation = assert.CallTrackerReportInformation;
- }
- const strict:
- & Omit<
- typeof assert,
- | "equal"
- | "notEqual"
- | "deepEqual"
- | "notDeepEqual"
- | "ok"
- | "strictEqual"
- | "deepStrictEqual"
- | "ifError"
- | "strict"
- | "AssertionError"
- >
- & {
- (value: unknown, message?: string | Error): asserts value;
- equal: typeof strictEqual;
- notEqual: typeof notStrictEqual;
- deepEqual: typeof deepStrictEqual;
- notDeepEqual: typeof notDeepStrictEqual;
- // Mapped types and assertion functions are incompatible?
- // TS2775: Assertions require every name in the call target
- // to be declared with an explicit type annotation.
- ok: typeof ok;
- strictEqual: typeof strictEqual;
- deepStrictEqual: typeof deepStrictEqual;
- ifError: typeof ifError;
- strict: typeof strict;
- AssertionError: typeof AssertionError;
- };
- }
- export = assert;
-}
-declare module "node:assert" {
- import assert = require("assert");
- export = assert;
-}
diff --git a/node_modules/@types/node/assert/strict.d.ts b/node_modules/@types/node/assert/strict.d.ts
deleted file mode 100644
index f333913..0000000
--- a/node_modules/@types/node/assert/strict.d.ts
+++ /dev/null
@@ -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;
-}
diff --git a/node_modules/@types/node/async_hooks.d.ts b/node_modules/@types/node/async_hooks.d.ts
deleted file mode 100644
index 2377689..0000000
--- a/node_modules/@types/node/async_hooks.d.ts
+++ /dev/null
@@ -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 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 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(
- 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 {
- /**
- * 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 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(): (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(store: T, callback: () => R): R;
- run(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(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";
-}
diff --git a/node_modules/@types/node/buffer.buffer.d.ts b/node_modules/@types/node/buffer.buffer.d.ts
deleted file mode 100644
index b22f83a..0000000
--- a/node_modules/@types/node/buffer.buffer.d.ts
+++ /dev/null
@@ -1,463 +0,0 @@
-declare module "buffer" {
- type ImplicitArrayBuffer> = 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;
- /**
- * 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;
- /**
- * 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): Buffer;
- /**
- * 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(arrayBuffer: TArrayBuffer): Buffer;
- /**
- * 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>): Buffer;
- /**
- * 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:
- *
- * // Changing the original Uint16Array changes the Buffer also.
- * arr[1] = 6000;
- *
- * console.log(buf);
- * // Prints:
- * ```
- *
- * 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:
- * ```
- * @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>(
- arrayBuffer: TArrayBuffer,
- byteOffset?: number,
- length?: number,
- ): Buffer>;
- /**
- * 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, encoding?: BufferEncoding): Buffer;
- from(arrayOrString: WithImplicitCoercion | string>): Buffer;
- /**
- * Creates a new Buffer using the passed {data}
- * @param values to create a new Buffer
- */
- of(...items: number[]): Buffer;
- /**
- * 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:
- * 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;
- /**
- * 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;
- /**
- * 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:
- * ```
- *
- * 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:
- * ```
- *
- * 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:
- * ```
- *
- * 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;
- /**
- * 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):
- *
- * buf.fill(0);
- *
- * console.log(buf);
- * // Prints:
- * ```
- *
- * 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;
- /**
- * 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;
- }
- interface Buffer extends Uint8Array {
- // 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;
- /**
- * 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;
- }
- type NonSharedBuffer = Buffer;
- type AllowSharedBuffer = Buffer;
- }
- /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */
- var SlowBuffer: {
- /** @deprecated Use `Buffer.allocUnsafeSlow()` instead. */
- new(size: number): Buffer;
- prototype: Buffer;
- };
-}
diff --git a/node_modules/@types/node/buffer.d.ts b/node_modules/@types/node/buffer.d.ts
deleted file mode 100644
index 49636f3..0000000
--- a/node_modules/@types/node/buffer.d.ts
+++ /dev/null
@@ -1,1930 +0,0 @@
-// If lib.dom.d.ts or lib.webworker.d.ts is loaded, then use the global types.
-// Otherwise, use the types from node.
-type _Blob = typeof globalThis extends { onmessage: any; Blob: any } ? {} : import("buffer").Blob;
-type _File = typeof globalThis extends { onmessage: any; File: any } ? {} : import("buffer").File;
-
-/**
- * `Buffer` objects are used to represent a fixed-length sequence of bytes. Many
- * Node.js APIs support `Buffer`s.
- *
- * The `Buffer` class is a subclass of JavaScript's [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) class and
- * extends it with methods that cover additional use cases. Node.js APIs accept
- * plain [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) s wherever `Buffer`s are supported as well.
- *
- * While the `Buffer` class is available within the global scope, it is still
- * recommended to explicitly reference it via an import or require statement.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * // Creates a zero-filled Buffer of length 10.
- * const buf1 = Buffer.alloc(10);
- *
- * // Creates a Buffer of length 10,
- * // filled with bytes which all have the value `1`.
- * const buf2 = Buffer.alloc(10, 1);
- *
- * // Creates an uninitialized buffer of length 10.
- * // This is faster than calling Buffer.alloc() but the returned
- * // Buffer instance might contain old data that needs to be
- * // overwritten using fill(), write(), or other functions that fill the Buffer's
- * // contents.
- * const buf3 = Buffer.allocUnsafe(10);
- *
- * // Creates a Buffer containing the bytes [1, 2, 3].
- * const buf4 = Buffer.from([1, 2, 3]);
- *
- * // Creates a Buffer containing the bytes [1, 1, 1, 1] – the entries
- * // are all truncated using `(value & 255)` to fit into the range 0–255.
- * const buf5 = Buffer.from([257, 257.5, -255, '1']);
- *
- * // Creates a Buffer containing the UTF-8-encoded bytes for the string 'tést':
- * // [0x74, 0xc3, 0xa9, 0x73, 0x74] (in hexadecimal notation)
- * // [116, 195, 169, 115, 116] (in decimal notation)
- * const buf6 = Buffer.from('tést');
- *
- * // Creates a Buffer containing the Latin-1 bytes [0x74, 0xe9, 0x73, 0x74].
- * const buf7 = Buffer.from('tést', 'latin1');
- * ```
- * @see [source](https://github.com/nodejs/node/blob/v24.x/lib/buffer.js)
- */
-declare module "buffer" {
- import { BinaryLike } from "node:crypto";
- import { ReadableStream as WebReadableStream } from "node:stream/web";
- /**
- * This function returns `true` if `input` contains only valid UTF-8-encoded data,
- * including the case in which `input` is empty.
- *
- * Throws if the `input` is a detached array buffer.
- * @since v19.4.0, v18.14.0
- * @param input The input to validate.
- */
- export function isUtf8(input: Buffer | ArrayBuffer | NodeJS.TypedArray): boolean;
- /**
- * This function returns `true` if `input` contains only valid ASCII-encoded data,
- * including the case in which `input` is empty.
- *
- * Throws if the `input` is a detached array buffer.
- * @since v19.6.0, v18.15.0
- * @param input The input to validate.
- */
- export function isAscii(input: Buffer | ArrayBuffer | NodeJS.TypedArray): boolean;
- export let INSPECT_MAX_BYTES: number;
- export const kMaxLength: number;
- export const kStringMaxLength: number;
- export const constants: {
- MAX_LENGTH: number;
- MAX_STRING_LENGTH: number;
- };
- export type TranscodeEncoding =
- | "ascii"
- | "utf8"
- | "utf-8"
- | "utf16le"
- | "utf-16le"
- | "ucs2"
- | "ucs-2"
- | "latin1"
- | "binary";
- /**
- * Re-encodes the given `Buffer` or `Uint8Array` instance from one character
- * encoding to another. Returns a new `Buffer` instance.
- *
- * Throws if the `fromEnc` or `toEnc` specify invalid character encodings or if
- * conversion from `fromEnc` to `toEnc` is not permitted.
- *
- * Encodings supported by `buffer.transcode()` are: `'ascii'`, `'utf8'`, `'utf16le'`, `'ucs2'`, `'latin1'`, and `'binary'`.
- *
- * The transcoding process will use substitution characters if a given byte
- * sequence cannot be adequately represented in the target encoding. For instance:
- *
- * ```js
- * import { Buffer, transcode } from 'node:buffer';
- *
- * const newBuf = transcode(Buffer.from('€'), 'utf8', 'ascii');
- * console.log(newBuf.toString('ascii'));
- * // Prints: '?'
- * ```
- *
- * Because the Euro (`€`) sign is not representable in US-ASCII, it is replaced
- * with `?` in the transcoded `Buffer`.
- * @since v7.1.0
- * @param source A `Buffer` or `Uint8Array` instance.
- * @param fromEnc The current encoding.
- * @param toEnc To target encoding.
- */
- export function transcode(source: Uint8Array, fromEnc: TranscodeEncoding, toEnc: TranscodeEncoding): Buffer;
- /**
- * Resolves a `'blob:nodedata:...'` an associated `Blob` object registered using
- * a prior call to `URL.createObjectURL()`.
- * @since v16.7.0
- * @param id A `'blob:nodedata:...` URL string returned by a prior call to `URL.createObjectURL()`.
- */
- export function resolveObjectURL(id: string): Blob | undefined;
- export { type AllowSharedBuffer, Buffer, type NonSharedBuffer };
- /**
- * @experimental
- */
- export interface BlobOptions {
- /**
- * One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts
- * will be converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`.
- */
- endings?: "transparent" | "native";
- /**
- * The Blob content-type. The intent is for `type` to convey
- * the MIME media type of the data, however no validation of the type format
- * is performed.
- */
- type?: string | undefined;
- }
- /**
- * A `Blob` encapsulates immutable, raw data that can be safely shared across
- * multiple worker threads.
- * @since v15.7.0, v14.18.0
- */
- export class Blob {
- /**
- * The total size of the `Blob` in bytes.
- * @since v15.7.0, v14.18.0
- */
- readonly size: number;
- /**
- * The content-type of the `Blob`.
- * @since v15.7.0, v14.18.0
- */
- readonly type: string;
- /**
- * Creates a new `Blob` object containing a concatenation of the given sources.
- *
- * {ArrayBuffer}, {TypedArray}, {DataView}, and {Buffer} sources are copied into
- * the 'Blob' and can therefore be safely modified after the 'Blob' is created.
- *
- * String sources are also copied into the `Blob`.
- */
- constructor(sources: Array, options?: BlobOptions);
- /**
- * Returns a promise that fulfills with an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) containing a copy of
- * the `Blob` data.
- * @since v15.7.0, v14.18.0
- */
- arrayBuffer(): Promise;
- /**
- * The `blob.bytes()` method returns the byte of the `Blob` object as a `Promise`.
- *
- * ```js
- * const blob = new Blob(['hello']);
- * blob.bytes().then((bytes) => {
- * console.log(bytes); // Outputs: Uint8Array(5) [ 104, 101, 108, 108, 111 ]
- * });
- * ```
- */
- bytes(): Promise;
- /**
- * Creates and returns a new `Blob` containing a subset of this `Blob` objects
- * data. The original `Blob` is not altered.
- * @since v15.7.0, v14.18.0
- * @param start The starting index.
- * @param end The ending index.
- * @param type The content-type for the new `Blob`
- */
- slice(start?: number, end?: number, type?: string): Blob;
- /**
- * Returns a promise that fulfills with the contents of the `Blob` decoded as a
- * UTF-8 string.
- * @since v15.7.0, v14.18.0
- */
- text(): Promise;
- /**
- * Returns a new `ReadableStream` that allows the content of the `Blob` to be read.
- * @since v16.7.0
- */
- stream(): WebReadableStream;
- }
- export interface FileOptions {
- /**
- * One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts will be
- * converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`.
- */
- endings?: "native" | "transparent";
- /** The File content-type. */
- type?: string;
- /** The last modified date of the file. `Default`: Date.now(). */
- lastModified?: number;
- }
- /**
- * A [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) provides information about files.
- * @since v19.2.0, v18.13.0
- */
- export class File extends Blob {
- constructor(sources: Array, fileName: string, options?: FileOptions);
- /**
- * The name of the `File`.
- * @since v19.2.0, v18.13.0
- */
- readonly name: string;
- /**
- * The last modified date of the `File`.
- * @since v19.2.0, v18.13.0
- */
- readonly lastModified: number;
- }
- export import atob = globalThis.atob;
- export import btoa = globalThis.btoa;
- export type WithImplicitCoercion =
- | T
- | { valueOf(): T }
- | (T extends string ? { [Symbol.toPrimitive](hint: "string"): T } : never);
- global {
- namespace NodeJS {
- export { BufferEncoding };
- }
- // Buffer class
- type BufferEncoding =
- | "ascii"
- | "utf8"
- | "utf-8"
- | "utf16le"
- | "utf-16le"
- | "ucs2"
- | "ucs-2"
- | "base64"
- | "base64url"
- | "latin1"
- | "binary"
- | "hex";
- /**
- * Raw data is stored in instances of the Buffer class.
- * A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.
- * Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'base64url'|'binary'(deprecated)|'hex'
- */
- interface BufferConstructor {
- // see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later
- // see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier
-
- /**
- * Returns `true` if `obj` is a `Buffer`, `false` otherwise.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * Buffer.isBuffer(Buffer.alloc(10)); // true
- * Buffer.isBuffer(Buffer.from('foo')); // true
- * Buffer.isBuffer('a string'); // false
- * Buffer.isBuffer([]); // false
- * Buffer.isBuffer(new Uint8Array(1024)); // false
- * ```
- * @since v0.1.101
- */
- isBuffer(obj: any): obj is Buffer;
- /**
- * Returns `true` if `encoding` is the name of a supported character encoding,
- * or `false` otherwise.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * console.log(Buffer.isEncoding('utf8'));
- * // Prints: true
- *
- * console.log(Buffer.isEncoding('hex'));
- * // Prints: true
- *
- * console.log(Buffer.isEncoding('utf/8'));
- * // Prints: false
- *
- * console.log(Buffer.isEncoding(''));
- * // Prints: false
- * ```
- * @since v0.9.1
- * @param encoding A character encoding name to check.
- */
- isEncoding(encoding: string): encoding is BufferEncoding;
- /**
- * Returns the byte length of a string when encoded using `encoding`.
- * This is not the same as [`String.prototype.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), which does not account
- * for the encoding that is used to convert the string into bytes.
- *
- * For `'base64'`, `'base64url'`, and `'hex'`, this function assumes valid input.
- * For strings that contain non-base64/hex-encoded data (e.g. whitespace), the
- * return value might be greater than the length of a `Buffer` created from the
- * string.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const str = '\u00bd + \u00bc = \u00be';
- *
- * console.log(`${str}: ${str.length} characters, ` +
- * `${Buffer.byteLength(str, 'utf8')} bytes`);
- * // Prints: ½ + ¼ = ¾: 9 characters, 12 bytes
- * ```
- *
- * When `string` is a
- * `Buffer`/[`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)/[`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/-
- * Reference/Global_Objects/TypedArray)/[`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)/[`SharedArrayBuffer`](https://develop-
- * er.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer), the byte length as reported by `.byteLength`is returned.
- * @since v0.1.90
- * @param string A value to calculate the length of.
- * @param [encoding='utf8'] If `string` is a string, this is its encoding.
- * @return The number of bytes contained within `string`.
- */
- byteLength(
- string: string | Buffer | NodeJS.ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
- encoding?: BufferEncoding,
- ): number;
- /**
- * Compares `buf1` to `buf2`, typically for the purpose of sorting arrays of `Buffer` instances. This is equivalent to calling `buf1.compare(buf2)`.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf1 = Buffer.from('1234');
- * const buf2 = Buffer.from('0123');
- * const arr = [buf1, buf2];
- *
- * console.log(arr.sort(Buffer.compare));
- * // Prints: [ , ]
- * // (This result is equal to: [buf2, buf1].)
- * ```
- * @since v0.11.13
- * @return Either `-1`, `0`, or `1`, depending on the result of the comparison. See `compare` for details.
- */
- compare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1;
- /**
- * This is the size (in bytes) of pre-allocated internal `Buffer` instances used
- * for pooling. This value may be modified.
- * @since v0.11.3
- */
- poolSize: number;
- }
- interface Buffer {
- // see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later
- // see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier
-
- /**
- * Writes `string` to `buf` at `offset` according to the character encoding in`encoding`. The `length` parameter is the number of bytes to write. If `buf` did
- * not contain enough space to fit the entire string, only part of `string` will be
- * written. However, partially encoded characters will not be written.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.alloc(256);
- *
- * const len = buf.write('\u00bd + \u00bc = \u00be', 0);
- *
- * console.log(`${len} bytes: ${buf.toString('utf8', 0, len)}`);
- * // Prints: 12 bytes: ½ + ¼ = ¾
- *
- * const buffer = Buffer.alloc(10);
- *
- * const length = buffer.write('abcd', 8);
- *
- * console.log(`${length} bytes: ${buffer.toString('utf8', 8, 10)}`);
- * // Prints: 2 bytes : ab
- * ```
- * @since v0.1.90
- * @param string String to write to `buf`.
- * @param [offset=0] Number of bytes to skip before starting to write `string`.
- * @param [length=buf.length - offset] Maximum number of bytes to write (written bytes will not exceed `buf.length - offset`).
- * @param [encoding='utf8'] The character encoding of `string`.
- * @return Number of bytes written.
- */
- write(string: string, encoding?: BufferEncoding): number;
- write(string: string, offset: number, encoding?: BufferEncoding): number;
- write(string: string, offset: number, length: number, encoding?: BufferEncoding): number;
- /**
- * Decodes `buf` to a string according to the specified character encoding in`encoding`. `start` and `end` may be passed to decode only a subset of `buf`.
- *
- * If `encoding` is `'utf8'` and a byte sequence in the input is not valid UTF-8,
- * then each invalid byte is replaced with the replacement character `U+FFFD`.
- *
- * The maximum length of a string instance (in UTF-16 code units) is available
- * as {@link constants.MAX_STRING_LENGTH}.
- *
- * ```js
- * import { Buffer } from 'node: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;
- * }
- *
- * console.log(buf1.toString('utf8'));
- * // Prints: abcdefghijklmnopqrstuvwxyz
- * console.log(buf1.toString('utf8', 0, 5));
- * // Prints: abcde
- *
- * const buf2 = Buffer.from('tést');
- *
- * console.log(buf2.toString('hex'));
- * // Prints: 74c3a97374
- * console.log(buf2.toString('utf8', 0, 3));
- * // Prints: té
- * console.log(buf2.toString(undefined, 0, 3));
- * // Prints: té
- * ```
- * @since v0.1.90
- * @param [encoding='utf8'] The character encoding to use.
- * @param [start=0] The byte offset to start decoding at.
- * @param [end=buf.length] The byte offset to stop decoding at (not inclusive).
- */
- toString(encoding?: BufferEncoding, start?: number, end?: number): string;
- /**
- * Returns a JSON representation of `buf`. [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) implicitly calls
- * this function when stringifying a `Buffer` instance.
- *
- * `Buffer.from()` accepts objects in the format returned from this method.
- * In particular, `Buffer.from(buf.toJSON())` works like `Buffer.from(buf)`.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]);
- * const json = JSON.stringify(buf);
- *
- * console.log(json);
- * // Prints: {"type":"Buffer","data":[1,2,3,4,5]}
- *
- * const copy = JSON.parse(json, (key, value) => {
- * return value && value.type === 'Buffer' ?
- * Buffer.from(value) :
- * value;
- * });
- *
- * console.log(copy);
- * // Prints:
- * ```
- * @since v0.9.2
- */
- toJSON(): {
- type: "Buffer";
- data: number[];
- };
- /**
- * Returns `true` if both `buf` and `otherBuffer` have exactly the same bytes,`false` otherwise. Equivalent to `buf.compare(otherBuffer) === 0`.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf1 = Buffer.from('ABC');
- * const buf2 = Buffer.from('414243', 'hex');
- * const buf3 = Buffer.from('ABCD');
- *
- * console.log(buf1.equals(buf2));
- * // Prints: true
- * console.log(buf1.equals(buf3));
- * // Prints: false
- * ```
- * @since v0.11.13
- * @param otherBuffer A `Buffer` or {@link Uint8Array} with which to compare `buf`.
- */
- equals(otherBuffer: Uint8Array): boolean;
- /**
- * Compares `buf` with `target` and returns a number indicating whether `buf`comes before, after, or is the same as `target` in sort order.
- * Comparison is based on the actual sequence of bytes in each `Buffer`.
- *
- * * `0` is returned if `target` is the same as `buf`
- * * `1` is returned if `target` should come _before_`buf` when sorted.
- * * `-1` is returned if `target` should come _after_`buf` when sorted.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf1 = Buffer.from('ABC');
- * const buf2 = Buffer.from('BCD');
- * const buf3 = Buffer.from('ABCD');
- *
- * console.log(buf1.compare(buf1));
- * // Prints: 0
- * console.log(buf1.compare(buf2));
- * // Prints: -1
- * console.log(buf1.compare(buf3));
- * // Prints: -1
- * console.log(buf2.compare(buf1));
- * // Prints: 1
- * console.log(buf2.compare(buf3));
- * // Prints: 1
- * console.log([buf1, buf2, buf3].sort(Buffer.compare));
- * // Prints: [ , , ]
- * // (This result is equal to: [buf1, buf3, buf2].)
- * ```
- *
- * The optional `targetStart`, `targetEnd`, `sourceStart`, and `sourceEnd` arguments can be used to limit the comparison to specific ranges within `target` and `buf` respectively.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf1 = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9]);
- * const buf2 = Buffer.from([5, 6, 7, 8, 9, 1, 2, 3, 4]);
- *
- * console.log(buf1.compare(buf2, 5, 9, 0, 4));
- * // Prints: 0
- * console.log(buf1.compare(buf2, 0, 6, 4));
- * // Prints: -1
- * console.log(buf1.compare(buf2, 5, 6, 5));
- * // Prints: 1
- * ```
- *
- * `ERR_OUT_OF_RANGE` is thrown if `targetStart < 0`, `sourceStart < 0`, `targetEnd > target.byteLength`, or `sourceEnd > source.byteLength`.
- * @since v0.11.13
- * @param target A `Buffer` or {@link Uint8Array} with which to compare `buf`.
- * @param [targetStart=0] The offset within `target` at which to begin comparison.
- * @param [targetEnd=target.length] The offset within `target` at which to end comparison (not inclusive).
- * @param [sourceStart=0] The offset within `buf` at which to begin comparison.
- * @param [sourceEnd=buf.length] The offset within `buf` at which to end comparison (not inclusive).
- */
- compare(
- target: Uint8Array,
- targetStart?: number,
- targetEnd?: number,
- sourceStart?: number,
- sourceEnd?: number,
- ): -1 | 0 | 1;
- /**
- * Copies data from a region of `buf` to a region in `target`, even if the `target`memory region overlaps with `buf`.
- *
- * [`TypedArray.prototype.set()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/set) performs the same operation, and is available
- * for all TypedArrays, including Node.js `Buffer`s, although it takes
- * different function arguments.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * // Create two `Buffer` instances.
- * const buf1 = Buffer.allocUnsafe(26);
- * const buf2 = Buffer.allocUnsafe(26).fill('!');
- *
- * for (let i = 0; i < 26; i++) {
- * // 97 is the decimal ASCII value for 'a'.
- * buf1[i] = i + 97;
- * }
- *
- * // Copy `buf1` bytes 16 through 19 into `buf2` starting at byte 8 of `buf2`.
- * buf1.copy(buf2, 8, 16, 20);
- * // This is equivalent to:
- * // buf2.set(buf1.subarray(16, 20), 8);
- *
- * console.log(buf2.toString('ascii', 0, 25));
- * // Prints: !!!!!!!!qrst!!!!!!!!!!!!!
- * ```
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * // Create a `Buffer` and copy data from one region to an overlapping region
- * // within the same `Buffer`.
- *
- * const buf = Buffer.allocUnsafe(26);
- *
- * for (let i = 0; i < 26; i++) {
- * // 97 is the decimal ASCII value for 'a'.
- * buf[i] = i + 97;
- * }
- *
- * buf.copy(buf, 0, 4, 10);
- *
- * console.log(buf.toString());
- * // Prints: efghijghijklmnopqrstuvwxyz
- * ```
- * @since v0.1.90
- * @param target A `Buffer` or {@link Uint8Array} to copy into.
- * @param [targetStart=0] The offset within `target` at which to begin writing.
- * @param [sourceStart=0] The offset within `buf` from which to begin copying.
- * @param [sourceEnd=buf.length] The offset within `buf` at which to stop copying (not inclusive).
- * @return The number of bytes copied.
- */
- copy(target: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number;
- /**
- * Writes `value` to `buf` at the specified `offset` as big-endian.
- *
- * `value` is interpreted and written as a two's complement signed integer.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(8);
- *
- * buf.writeBigInt64BE(0x0102030405060708n, 0);
- *
- * console.log(buf);
- * // Prints:
- * ```
- * @since v12.0.0, v10.20.0
- * @param value Number to be written to `buf`.
- * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`.
- * @return `offset` plus the number of bytes written.
- */
- writeBigInt64BE(value: bigint, offset?: number): number;
- /**
- * Writes `value` to `buf` at the specified `offset` as little-endian.
- *
- * `value` is interpreted and written as a two's complement signed integer.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(8);
- *
- * buf.writeBigInt64LE(0x0102030405060708n, 0);
- *
- * console.log(buf);
- * // Prints:
- * ```
- * @since v12.0.0, v10.20.0
- * @param value Number to be written to `buf`.
- * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`.
- * @return `offset` plus the number of bytes written.
- */
- writeBigInt64LE(value: bigint, offset?: number): number;
- /**
- * Writes `value` to `buf` at the specified `offset` as big-endian.
- *
- * This function is also available under the `writeBigUint64BE` alias.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(8);
- *
- * buf.writeBigUInt64BE(0xdecafafecacefaden, 0);
- *
- * console.log(buf);
- * // Prints:
- * ```
- * @since v12.0.0, v10.20.0
- * @param value Number to be written to `buf`.
- * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`.
- * @return `offset` plus the number of bytes written.
- */
- writeBigUInt64BE(value: bigint, offset?: number): number;
- /**
- * @alias Buffer.writeBigUInt64BE
- * @since v14.10.0, v12.19.0
- */
- writeBigUint64BE(value: bigint, offset?: number): number;
- /**
- * Writes `value` to `buf` at the specified `offset` as little-endian
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(8);
- *
- * buf.writeBigUInt64LE(0xdecafafecacefaden, 0);
- *
- * console.log(buf);
- * // Prints:
- * ```
- *
- * This function is also available under the `writeBigUint64LE` alias.
- * @since v12.0.0, v10.20.0
- * @param value Number to be written to `buf`.
- * @param [offset=0] Number of bytes to skip before starting to write. Must satisfy: `0 <= offset <= buf.length - 8`.
- * @return `offset` plus the number of bytes written.
- */
- writeBigUInt64LE(value: bigint, offset?: number): number;
- /**
- * @alias Buffer.writeBigUInt64LE
- * @since v14.10.0, v12.19.0
- */
- writeBigUint64LE(value: bigint, offset?: number): number;
- /**
- * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined
- * when `value` is anything other than an unsigned integer.
- *
- * This function is also available under the `writeUintLE` alias.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(6);
- *
- * buf.writeUIntLE(0x1234567890ab, 0, 6);
- *
- * console.log(buf);
- * // Prints:
- * ```
- * @since v0.5.5
- * @param value Number to be written to `buf`.
- * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`.
- * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`.
- * @return `offset` plus the number of bytes written.
- */
- writeUIntLE(value: number, offset: number, byteLength: number): number;
- /**
- * @alias Buffer.writeUIntLE
- * @since v14.9.0, v12.19.0
- */
- writeUintLE(value: number, offset: number, byteLength: number): number;
- /**
- * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as big-endian. Supports up to 48 bits of accuracy. Behavior is undefined
- * when `value` is anything other than an unsigned integer.
- *
- * This function is also available under the `writeUintBE` alias.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(6);
- *
- * buf.writeUIntBE(0x1234567890ab, 0, 6);
- *
- * console.log(buf);
- * // Prints:
- * ```
- * @since v0.5.5
- * @param value Number to be written to `buf`.
- * @param offset Number of bytes to skip before starting to write. Must satisfy `0 <= offset <= buf.length - byteLength`.
- * @param byteLength Number of bytes to write. Must satisfy `0 < byteLength <= 6`.
- * @return `offset` plus the number of bytes written.
- */
- writeUIntBE(value: number, offset: number, byteLength: number): number;
- /**
- * @alias Buffer.writeUIntBE
- * @since v14.9.0, v12.19.0
- */
- writeUintBE(value: number, offset: number, byteLength: number): number;
- /**
- * Writes `byteLength` bytes of `value` to `buf` at the specified `offset`as little-endian. Supports up to 48 bits of accuracy. Behavior is undefined
- * when `value` is anything other than a signed integer.
- *
- * ```js
- * import { Buffer } from 'node:buffer';
- *
- * const buf = Buffer.allocUnsafe(6);
- *
- * buf.writeIntLE(0x1234567890ab, 0, 6);
- *
- * console.log(buf);
- * // Prints: