main commit
This commit is contained in:
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-10-04T02:36:57.095708689Z">
|
||||
<DropdownSelection timestamp="2025-10-05T21:09:42.524082016Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=LGMG600S9b4da66b" />
|
||||
|
||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -6,4 +6,11 @@
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
<component name="VisualizationToolProject">
|
||||
<option name="state">
|
||||
<ProjectState>
|
||||
<option name="scale" value="0.19048339843749995" />
|
||||
</ProjectState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
78
.kotlin/errors/errors-1759565614541.log
Normal file
78
.kotlin/errors/errors-1759565614541.log
Normal file
@@ -0,0 +1,78 @@
|
||||
kotlin version: 2.0.20
|
||||
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:15:5: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
|
||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
|
||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
|
||||
at java.base/java.lang.Thread.run(Thread.java:840)
|
||||
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||
... 35 more
|
||||
|
||||
|
||||
78
.kotlin/errors/errors-1759565654456.log
Normal file
78
.kotlin/errors/errors-1759565654456.log
Normal file
@@ -0,0 +1,78 @@
|
||||
kotlin version: 2.0.20
|
||||
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:15:5: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
|
||||
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
|
||||
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
|
||||
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
|
||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
|
||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
|
||||
at java.base/java.lang.Thread.run(Thread.java:840)
|
||||
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||
... 35 more
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("src/main/AndroidManifest-legacy.xml")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.godeye"
|
||||
compileSdk = 29 // Понижаем до Android 10
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.godeye"
|
||||
minSdk = 24
|
||||
targetSdk = 28 // Понижаем до Android 9
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0-legacy"
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -28,60 +29,72 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "11"
|
||||
}
|
||||
|
||||
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
|
||||
buildFeatures {
|
||||
compose = false
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.15"
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для 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
|
||||
// Core Android
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.preference)
|
||||
|
||||
// Классический 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
|
||||
// Compose
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
|
||||
// СТАРЫЕ 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
|
||||
// Material Icons
|
||||
implementation("androidx.compose.material:material-icons-core")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
|
||||
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
|
||||
// Вместо CameraX используем старую Camera2 API напрямую
|
||||
// Coroutines
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
// Socket.IO и базовые сетевые библиотеки
|
||||
implementation("io.socket:socket.io-client:2.1.0")
|
||||
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
|
||||
// Network
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.socketio.client)
|
||||
|
||||
// УБИРАЕМ WebRTC полностью для стабильности
|
||||
// implementation("io.getstream:stream-webrtc-android:1.0.4")
|
||||
// JSON
|
||||
implementation(libs.gson)
|
||||
|
||||
// Старые корутины
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год
|
||||
|
||||
// Базовые зависимости - старые версии
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год
|
||||
|
||||
// УБИРАЕМ Work Manager и Activity KTX
|
||||
// implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||
// implementation("androidx.activity:activity-ktx:1.7.2")
|
||||
// WebRTC
|
||||
implementation(libs.webrtc.android)
|
||||
|
||||
// Testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3") // Старая версия
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") // Старая версия
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
||||
|
||||
87
app/build.gradle.kts.backup
Normal file
87
app/build.gradle.kts.backup
Normal file
@@ -0,0 +1,87 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.godeye"
|
||||
compileSdk = 29 // Понижаем до Android 10
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.godeye"
|
||||
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"
|
||||
}
|
||||
|
||||
// ОТКЛЮЧАЕМ COMPOSE ДЛЯ LEGACY ВЕРСИИ
|
||||
buildFeatures {
|
||||
compose = false
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// ЭКСТРЕМАЛЬНО СТАРЫЕ зависимости для 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
|
||||
|
||||
// Классический 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
|
||||
|
||||
// СТАРЫЕ 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
|
||||
|
||||
// УБИРАЕМ СОВРЕМЕННЫЕ CAMERA БИБЛИОТЕКИ
|
||||
// Вместо CameraX используем старую Camera2 API напрямую
|
||||
|
||||
// Socket.IO и базовые сетевые библиотеки
|
||||
implementation("io.socket:socket.io-client:2.1.0")
|
||||
implementation("com.google.code.gson:gson:2.8.9") // Старая версия
|
||||
|
||||
// УБИРАЕМ WebRTC полностью для стабильности
|
||||
// implementation("io.getstream:stream-webrtc-android:1.0.4")
|
||||
|
||||
// Старые корутины
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") // 2021 год
|
||||
|
||||
// Базовые зависимости - старые версии
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1") // 2021 год
|
||||
|
||||
// УБИРАЕМ Work Manager и Activity KTX
|
||||
// implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||
// implementation("androidx.activity:activity-ktx:1.7.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") // Старая версия
|
||||
}
|
||||
@@ -2,15 +2,22 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Разрешения согласно ТЗ -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<!-- Разрешения для сигналлинга и WebRTC -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Hardware features согласно ТЗ -->
|
||||
<!-- Разрешения для камеры и микрофона -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- Hardware features -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
@@ -26,9 +33,9 @@
|
||||
android:theme="@style/Theme.GodEye"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
tools:targetApi="35">
|
||||
|
||||
<!-- MainActivity - главный экран согласно ТЗ -->
|
||||
<!-- MainActivity с Compose интерфейсом -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -41,7 +48,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- LegacyMainActivity - упрощенная версия для Android 9 -->
|
||||
<!-- Legacy активности для совместимости -->
|
||||
<activity
|
||||
android:name=".LegacyMainActivity"
|
||||
android:exported="false"
|
||||
@@ -49,7 +56,6 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<!-- LegacyCameraActivity - камера для Android 9 -->
|
||||
<activity
|
||||
android:name=".LegacyCameraActivity"
|
||||
android:exported="false"
|
||||
@@ -57,12 +63,24 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<!-- SocketService - WebSocket соединение согласно ТЗ -->
|
||||
<!-- Сервисы -->
|
||||
<service
|
||||
android:name=".services.SocketService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.SignalingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".services.ConnectionService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
68
app/src/main/AndroidManifest.xml.backup
Normal file
68
app/src/main/AndroidManifest.xml.backup
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Разрешения согласно ТЗ -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!-- Hardware features согласно ТЗ -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
|
||||
<application
|
||||
android:name=".GodEyeApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.GodEye"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<!-- MainActivity - главный экран согласно ТЗ -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.GodEye"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- LegacyMainActivity - упрощенная версия для Android 9 -->
|
||||
<activity
|
||||
android:name=".LegacyMainActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.GodEye"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<!-- LegacyCameraActivity - камера для Android 9 -->
|
||||
<activity
|
||||
android:name=".LegacyCameraActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.GodEye"
|
||||
android:screenOrientation="portrait"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<!-- SocketService - WebSocket соединение согласно ТЗ -->
|
||||
<service
|
||||
android:name=".services.SocketService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -6,6 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
@@ -128,7 +129,11 @@ class LegacyMainActivity : AppCompatActivity() {
|
||||
private fun startAndBindService() {
|
||||
try {
|
||||
val intent = Intent(this, SocketService::class.java)
|
||||
startForegroundService(intent)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
Logger.step("LEGACY_SERVICE_BIND", "Binding to SocketService")
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,431 +1,176 @@
|
||||
package com.example.godeye
|
||||
|
||||
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 android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
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.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 androidx.lifecycle.lifecycleScope
|
||||
import com.example.godeye.managers.ConnectionManager
|
||||
import com.example.godeye.managers.AutoApprovalManager
|
||||
import com.example.godeye.models.*
|
||||
import com.example.godeye.services.SocketService
|
||||
import com.example.godeye.services.ConnectionService
|
||||
import com.example.godeye.ui.components.*
|
||||
import com.example.godeye.ui.theme.GodEyeColors
|
||||
import com.example.godeye.ui.screens.MainScreen
|
||||
import com.example.godeye.ui.screens.SettingsScreen
|
||||
import com.example.godeye.ui.theme.GodEyeTheme
|
||||
import com.example.godeye.utils.ErrorHandler
|
||||
import com.example.godeye.utils.Logger
|
||||
import com.example.godeye.utils.PermissionHelper
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* MainActivity - упрощенная версия для Android 9
|
||||
* БЕЗ сложных анимаций и градиентов
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private val errorHandler = ErrorHandler()
|
||||
private var socketService: SocketService? = null
|
||||
|
||||
// Подключение к 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!!)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Убираем SignalingManager - используем только ConnectionManager + AutoApprovalManager
|
||||
// private lateinit var signalingManager: SignalingManager
|
||||
private lateinit var connectionManager: ConnectionManager
|
||||
private lateinit var autoApprovalManager: AutoApprovalManager
|
||||
private lateinit var permissionHelper: PermissionHelper
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
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}")
|
||||
// Инициализируем менеджеры (БЕЗ SignalingManager)
|
||||
preferenceManager = PreferenceManager(this)
|
||||
// signalingManager = SignalingManager(this) // УБИРАЕМ
|
||||
connectionManager = ConnectionManager(this, preferenceManager)
|
||||
autoApprovalManager = AutoApprovalManager(this, preferenceManager, connectionManager)
|
||||
permissionHelper = PermissionHelper(this)
|
||||
|
||||
// Запуск SocketService
|
||||
startAndBindSocketService()
|
||||
|
||||
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()
|
||||
|
||||
// Автоматическое принятие запросов камеры
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
GodEyeTheme {
|
||||
AppContent()
|
||||
}
|
||||
|
||||
// Проверка разрешений
|
||||
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)
|
||||
}
|
||||
// Проверяем разрешения при запуске
|
||||
checkPermissionsAndStart()
|
||||
|
||||
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()
|
||||
}
|
||||
// Запускаем фоновый сервис
|
||||
startConnectionService()
|
||||
}
|
||||
|
||||
@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()
|
||||
private fun AppContent() {
|
||||
var currentScreen by remember { mutableStateOf(Screen.Main) }
|
||||
|
||||
// УПРОЩЕННЫЙ 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)
|
||||
}
|
||||
when (currentScreen) {
|
||||
Screen.Main -> {
|
||||
MainScreen(
|
||||
connectionManager = connectionManager,
|
||||
autoApprovalManager = autoApprovalManager,
|
||||
preferenceManager = preferenceManager,
|
||||
permissionHelper = permissionHelper,
|
||||
onOpenSettings = { currentScreen = Screen.Settings }
|
||||
)
|
||||
) {
|
||||
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)
|
||||
Screen.Settings -> {
|
||||
SettingsScreen(
|
||||
preferenceManager = preferenceManager,
|
||||
permissionHelper = permissionHelper,
|
||||
onNavigateBack = { currentScreen = Screen.Main }
|
||||
)
|
||||
) {
|
||||
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("⚙️ Настройки")
|
||||
}
|
||||
}
|
||||
private enum class Screen {
|
||||
Main, Settings
|
||||
}
|
||||
|
||||
// Кнопка для запуска 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 checkPermissionsAndStart() {
|
||||
if (permissionHelper.hasAllPermissions()) {
|
||||
// Все разрешения уже есть, можно работать
|
||||
Log.d("MainActivity", "All permissions granted")
|
||||
startApplication()
|
||||
} else {
|
||||
// Запрашиваем разрешения
|
||||
Log.d("MainActivity", "Requesting permissions")
|
||||
permissionHelper.requestPermissions { granted ->
|
||||
if (granted) {
|
||||
Log.d("MainActivity", "Permissions granted, starting application")
|
||||
startApplication()
|
||||
} else {
|
||||
Log.w("MainActivity", "Some permissions denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startApplication() {
|
||||
// Инициализируем автоматическое подключение если включено
|
||||
if (preferenceManager.getAutoConnect()) {
|
||||
connectionManager.connect()
|
||||
}
|
||||
|
||||
Log.d("MainActivity", "Application started with settings: ${preferenceManager.getAllSettings()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск фонового сервиса для поддержания подключения
|
||||
*/
|
||||
private fun startConnectionService() {
|
||||
val serviceIntent = Intent(this, ConnectionService::class.java).apply {
|
||||
action = ConnectionService.ACTION_START
|
||||
}
|
||||
|
||||
// Проверяем версию Android для совместимости
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
|
||||
Log.d("MainActivity", "Connection service started")
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка фонового сервиса
|
||||
*/
|
||||
private fun stopConnectionService() {
|
||||
val serviceIntent = Intent(this, ConnectionService::class.java).apply {
|
||||
action = ConnectionService.ACTION_STOP
|
||||
}
|
||||
stopService(serviceIntent)
|
||||
Log.d("MainActivity", "Connection service stopped")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.d("MainActivity", "Activity resumed")
|
||||
|
||||
// Проверяем настройки и автоподключение
|
||||
if (permissionHelper.hasAllPermissions() && preferenceManager.getAutoConnect()) {
|
||||
connectionManager.connect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d("MainActivity", "Activity paused - service continues in background")
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// Очищаем ресурсы Activity, но оставляем сервис работать
|
||||
connectionManager.cleanup()
|
||||
autoApprovalManager.cleanup()
|
||||
|
||||
// Останавливаем сервис только если приложение действительно закрывается
|
||||
if (isFinishing) {
|
||||
stopConnectionService()
|
||||
}
|
||||
|
||||
Log.d("MainActivity", "MainActivity destroyed, resources cleaned up")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
@@ -131,53 +130,62 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка наблюдателей за SocketService
|
||||
* Настройка наблюдателей за SocketService - исправленная версия
|
||||
*/
|
||||
private fun setupServiceObservers() {
|
||||
val service = socketService ?: return
|
||||
|
||||
// Простое наблюдение за состоянием подключения без внутренних API
|
||||
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
|
||||
while (true) {
|
||||
try {
|
||||
val state = service.connectionState.value
|
||||
if (_connectionState.value != state) {
|
||||
_connectionState.value = state
|
||||
Logger.step("CONNECTION_STATE_CHANGED", "Connection state: $state")
|
||||
}
|
||||
kotlinx.coroutines.delay(1000) // Проверяем каждую секунду
|
||||
} catch (e: Exception) {
|
||||
Logger.error("CONNECTION_STATE_ERROR", "Error checking connection state", e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Простое наблюдение за запросами камеры
|
||||
viewModelScope.launch {
|
||||
// Наблюдение за WebRTC событиями
|
||||
service.webRTCEvents.collect { event ->
|
||||
event?.let { handleWebRTCEvent(it) }
|
||||
while (true) {
|
||||
try {
|
||||
val request = service.cameraRequests.value
|
||||
if (_cameraRequest.value != request && request != null) {
|
||||
Logger.step("CAMERA_REQUEST_RECEIVED",
|
||||
"Camera request from ${request.operatorId} for ${request.cameraType}")
|
||||
_cameraRequest.value = request
|
||||
}
|
||||
kotlinx.coroutines.delay(500) // Проверяем каждые полсекунды
|
||||
} catch (e: Exception) {
|
||||
Logger.error("CAMERA_REQUEST_ERROR", "Error checking camera requests", e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Простое наблюдение за WebRTC событиями
|
||||
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))
|
||||
)
|
||||
while (true) {
|
||||
try {
|
||||
val event = service.webRTCEvents.value
|
||||
if (event != null) {
|
||||
handleWebRTCEvent(event)
|
||||
// Очищаем событие после обработки
|
||||
service.webRTCEvents as MutableStateFlow
|
||||
(service.webRTCEvents as MutableStateFlow).value = null
|
||||
}
|
||||
kotlinx.coroutines.delay(100) // Проверяем каждые 100мс
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_EVENT_ERROR", "Error checking WebRTC events", e)
|
||||
break
|
||||
}
|
||||
_activeSessions.value = sessionInfo
|
||||
_isStreaming.value = sessions.values.any { it.isActive }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,10 +194,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* Инициализация WebRTC согласно ТЗ
|
||||
*/
|
||||
private fun initializeWebRTC() {
|
||||
webRTCManager = WebRTCManager(context) { message ->
|
||||
// Обработка сигнальных сообщений через SocketService
|
||||
Logger.step("WEBRTC_SIGNALING", "WebRTC signaling message: ${message.getString("type")}")
|
||||
}
|
||||
webRTCManager = WebRTCManager(context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,7 +262,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
socketService?.sendCameraResponse(sessionId, true, reason)
|
||||
|
||||
// 3. Инициализация WebRTC соединения
|
||||
webRTCManager?.startStreaming(sessionId, request.cameraType)
|
||||
webRTCManager?.startStreaming(request.operatorId, request.cameraType)
|
||||
|
||||
// 4. Очистка запроса
|
||||
_cameraRequest.value = null
|
||||
@@ -287,21 +292,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
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.Connected -> {
|
||||
Logger.step("WEBRTC_CONNECTED", "WebRTC connected")
|
||||
}
|
||||
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.Disconnected -> {
|
||||
Logger.step("WEBRTC_DISCONNECTED", "WebRTC disconnected")
|
||||
}
|
||||
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.Error -> {
|
||||
Logger.step("WEBRTC_ERROR", "WebRTC error: ${event.message}")
|
||||
}
|
||||
is com.example.godeye.services.WebRTCEvent.SwitchCamera -> {
|
||||
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: ${event.cameraType}")
|
||||
switchCamera(event.cameraType)
|
||||
else -> {
|
||||
Logger.step("WEBRTC_EVENT", "Unknown WebRTC event: $event")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,7 +336,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
try {
|
||||
Logger.step("SWITCH_CAMERA", "Switching camera to: $cameraType")
|
||||
|
||||
webRTCManager?.switchCamera(cameraType)
|
||||
webRTCManager?.switchCamera()
|
||||
|
||||
// Обновляем тип камеры в активных сессиях
|
||||
val updatedSessions = _activeSessions.value.mapValues { (_, sessionInfo) ->
|
||||
@@ -358,7 +359,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
Logger.step("END_SESSION", "Ending camera session: $sessionId")
|
||||
|
||||
sessionManager.endSession(sessionId, "Ended by user")
|
||||
webRTCManager?.endSession(sessionId)
|
||||
webRTCManager?.endSession()
|
||||
|
||||
Logger.step("SESSION_ENDED", "Session ended: $sessionId")
|
||||
}
|
||||
@@ -404,34 +405,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
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 при получении разрешений
|
||||
*/
|
||||
@@ -471,14 +444,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
// Инициализируем WebRTC если нужно
|
||||
if (webRTCManager == null) {
|
||||
webRTCManager = WebRTCManager(context) { message ->
|
||||
// Обработка сигналинга для тестового режима
|
||||
Logger.d("Test signaling message: $message")
|
||||
}
|
||||
webRTCManager = WebRTCManager(context)
|
||||
}
|
||||
|
||||
// Запускаем стриминг
|
||||
webRTCManager?.startStreaming(testSessionId, "back")
|
||||
webRTCManager?.startStreaming("test_operator", "back")
|
||||
|
||||
Logger.step("START_TEST_STREAMING_SUCCESS", "Test streaming started successfully")
|
||||
|
||||
|
||||
@@ -1,364 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
package com.example.godeye.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.view.SurfaceView
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.example.godeye.ui.components.*
|
||||
import com.example.godeye.ui.theme.GodEyeColors
|
||||
import com.example.godeye.utils.Logger
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
fun CameraScreen(
|
||||
onBackPressed: () -> Unit,
|
||||
sessionId: String = "",
|
||||
@Suppress("UNUSED_PARAMETER") operatorId: String = ""
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
var hasPermissions by remember { mutableStateOf(false) }
|
||||
var showError by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// Упрощенный CameraManager без сложных анимаций
|
||||
val cameraManager = remember {
|
||||
try {
|
||||
CameraManager(context)
|
||||
} catch (e: Exception) {
|
||||
Logger.error("CAMERA_MANAGER_CREATE_ERROR", "Failed to create camera manager", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val previewView = remember {
|
||||
try {
|
||||
PreviewView(context).apply {
|
||||
scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("PREVIEW_VIEW_CREATE_ERROR", "Failed to create preview view", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка разрешений
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
hasPermissions = permissions.values.all { it }
|
||||
if (!hasPermissions) {
|
||||
showError = "Необходимы разрешения для работы с камерой"
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
hasPermissions = cameraManager?.hasPermissions() ?: false
|
||||
if (!hasPermissions) {
|
||||
permissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Простая инициализация камеры БЕЗ сложных анимаций
|
||||
LaunchedEffect(hasPermissions) {
|
||||
if (hasPermissions && cameraManager != null && previewView != null) {
|
||||
try {
|
||||
Logger.step("CAMERA_INIT_START", "Starting simple camera initialization")
|
||||
cameraManager.setupCamera(previewView, lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA)
|
||||
Logger.step("CAMERA_INIT_SUCCESS", "Simple camera initialized successfully")
|
||||
} catch (e: Exception) {
|
||||
showError = "Ошибка инициализации камеры: ${e.message}"
|
||||
Logger.error("CAMERA_INIT_ERROR", "Camera initialization failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Безопасное освобождение ресурсов при закрытии
|
||||
DisposableEffect(cameraManager) {
|
||||
onDispose {
|
||||
try {
|
||||
Logger.step("CAMERA_SCREEN_DISPOSE", "Disposing camera screen safely")
|
||||
cameraManager?.release()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("CAMERA_DISPOSE_ERROR", "Error disposing camera", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// УПРОЩЕННЫЙ UI БЕЗ СЛОЖНЫХ АНИМАЦИЙ
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
) {
|
||||
if (hasPermissions && previewView != null && cameraManager != null) {
|
||||
// Простой preview БЕЗ сложных эффектов
|
||||
AndroidView(
|
||||
factory = { previewView },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// Простая верхняя панель БЕЗ анимаций
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.Black.copy(alpha = 0.7f)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Button(
|
||||
onClick = onBackPressed,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Text("← Назад", color = Color.White)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "GodEye Camera",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(60.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// Простая нижняя панель БЕЗ анимаций
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.Black.copy(alpha = 0.7f)
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Простая кнопка фото БЕЗ анимаций
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val photoFile = File(
|
||||
context.externalCacheDir,
|
||||
"photo_${System.currentTimeMillis()}.jpg"
|
||||
)
|
||||
cameraManager.takePhoto(
|
||||
photoFile,
|
||||
onPhotoTaken = {
|
||||
Logger.step("PHOTO_TAKEN", "Photo taken successfully")
|
||||
},
|
||||
onError = { error ->
|
||||
showError = "Ошибка съемки: ${error.message}"
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
showError = "Ошибка съемки: ${e.message}"
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Blue
|
||||
)
|
||||
) {
|
||||
Text("📷 Фото", color = Color.White)
|
||||
}
|
||||
|
||||
// Простая кнопка переключения камеры БЕЗ анимаций
|
||||
Button(
|
||||
onClick = {
|
||||
try {
|
||||
val newCameraSelector = cameraManager.switchCamera()
|
||||
// Простое пересоздание камеры с новым селектором
|
||||
cameraManager.setupCamera(previewView, lifecycleOwner, newCameraSelector)
|
||||
} catch (e: Exception) {
|
||||
showError = "Ошибка переключения камеры"
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Green
|
||||
)
|
||||
) {
|
||||
Text("🔄 Камера", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Простой экран разрешений БЕЗ анимаций
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = if (cameraManager == null || previewView == null) "Ошибка камеры" else "Требуются разрешения",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (cameraManager != null && previewView != null) {
|
||||
Button(
|
||||
onClick = {
|
||||
permissionLauncher.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO
|
||||
)
|
||||
)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Blue
|
||||
)
|
||||
) {
|
||||
Text("Предоставить разрешения", color = Color.White)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = onBackPressed,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.Red
|
||||
)
|
||||
) {
|
||||
Text("Вернуться назад", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Простое отображение ошибок БЕЗ анимаций
|
||||
showError?.let { error ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.Red.copy(alpha = 0.9f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = error,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(
|
||||
onClick = { showError = null },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White.copy(alpha = 0.2f)
|
||||
)
|
||||
) {
|
||||
Text("Закрыть", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
package com.example.godeye.managers
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.godeye.R
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Менеджер для автоматической обработки запросов доступа к камере
|
||||
*/
|
||||
class AutoApprovalManager(
|
||||
private val context: Context,
|
||||
private val preferenceManager: PreferenceManager,
|
||||
private val connectionManager: ConnectionManager
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "AutoApprovalManager"
|
||||
private const val NOTIFICATION_CHANNEL_ID = "camera_requests"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Активные сессии камеры с дополнительными метриками для мониторинга
|
||||
data class CameraSession(
|
||||
val sessionId: String,
|
||||
val operatorId: String,
|
||||
val cameraType: String,
|
||||
val startTime: Long = System.currentTimeMillis(),
|
||||
val isAutoApproved: Boolean = false,
|
||||
// Дополнительные поля для архитектуры GodEye Signal Center
|
||||
val deviceId: String,
|
||||
val quality: String = "720p",
|
||||
var lastHeartbeat: Long = System.currentTimeMillis(),
|
||||
var streamingState: StreamingState = StreamingState.INITIALIZING
|
||||
)
|
||||
|
||||
enum class StreamingState {
|
||||
INITIALIZING,
|
||||
CONNECTING,
|
||||
STREAMING,
|
||||
PAUSED,
|
||||
ERROR,
|
||||
DISCONNECTED
|
||||
}
|
||||
|
||||
// Состояние системы GodEye Signal Center
|
||||
private val _systemState = MutableStateFlow(SystemState.READY)
|
||||
val systemState: StateFlow<SystemState> = _systemState.asStateFlow()
|
||||
|
||||
enum class SystemState {
|
||||
READY,
|
||||
PROCESSING_REQUEST,
|
||||
STREAMING_ACTIVE,
|
||||
ERROR,
|
||||
MAINTENANCE
|
||||
}
|
||||
|
||||
private val _activeSessions = MutableStateFlow<List<CameraSession>>(emptyList())
|
||||
val activeSessions: StateFlow<List<CameraSession>> = _activeSessions.asStateFlow()
|
||||
|
||||
// Слушатели для UI
|
||||
private val _pendingRequest = MutableStateFlow<JSONObject?>(null)
|
||||
val pendingRequest: StateFlow<JSONObject?> = _pendingRequest.asStateFlow()
|
||||
|
||||
init {
|
||||
createNotificationChannel()
|
||||
setupConnectionManagerListeners()
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка слушателей событий от ConnectionManager
|
||||
* Поддерживает обе конвенции именования: с двоеточиями (desktop/web) и дефисами (android)
|
||||
*/
|
||||
private fun setupConnectionManagerListeners() {
|
||||
Log.d(TAG, "Setting up connection manager listeners with dual naming convention support")
|
||||
|
||||
// Обработка запросов доступа к камере (поддержка обеих конвенций)
|
||||
val cameraRequestHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
|
||||
try {
|
||||
Log.d(TAG, "Received camera request event with ${data.size} elements")
|
||||
if (data.isNotEmpty()) {
|
||||
val requestData = data[0] as? JSONObject
|
||||
Log.d(TAG, "Request data: $requestData")
|
||||
requestData?.let { handleCameraRequest(it) }
|
||||
} else {
|
||||
Log.w(TAG, "camera request event received but data is empty")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing camera request event", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Регистрируем обработчики для обеих конвенций
|
||||
connectionManager.registerEventListener("camera:request", cameraRequestHandler) // Desktop/Web format
|
||||
connectionManager.registerEventListener("camera-request", cameraRequestHandler) // Android format
|
||||
|
||||
// Обработка автоматически одобренных запросов
|
||||
val autoApprovedHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
|
||||
try {
|
||||
if (data.isNotEmpty()) {
|
||||
val requestData = data[0] as? JSONObject
|
||||
requestData?.let { handleAutoApprovedRequest(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing camera auto-approved event", e)
|
||||
}
|
||||
}
|
||||
connectionManager.registerEventListener("camera:auto-approved", autoApprovedHandler)
|
||||
connectionManager.registerEventListener("camera-auto-approved", autoApprovedHandler)
|
||||
|
||||
// Обработка отключения камеры
|
||||
val disconnectHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
|
||||
try {
|
||||
if (data.isNotEmpty()) {
|
||||
val requestData = data[0] as? JSONObject
|
||||
requestData?.let { handleCameraDisconnect(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing camera disconnect event", e)
|
||||
}
|
||||
}
|
||||
connectionManager.registerEventListener("camera:disconnect", disconnectHandler)
|
||||
connectionManager.registerEventListener("camera-disconnect", disconnectHandler)
|
||||
|
||||
// Обработка WebRTC answer
|
||||
val webrtcAnswerHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
|
||||
try {
|
||||
if (data.isNotEmpty()) {
|
||||
val requestData = data[0] as? JSONObject
|
||||
requestData?.let {
|
||||
val sessionId = it.optString("sessionId")
|
||||
val answer = it.optString("answer")
|
||||
if (sessionId.isNotEmpty() && answer.isNotEmpty()) {
|
||||
Log.d(TAG, "Received WebRTC answer for session: $sessionId")
|
||||
connectionManager.handleWebRTCAnswer(sessionId, answer)
|
||||
} else {
|
||||
Log.w(TAG, "Invalid WebRTC answer data: sessionId=$sessionId, answer length=${answer.length}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing webrtc answer event", e)
|
||||
}
|
||||
}
|
||||
connectionManager.registerEventListener("webrtc:answer", webrtcAnswerHandler)
|
||||
connectionManager.registerEventListener("webrtc-answer", webrtcAnswerHandler)
|
||||
// Добавляем слушатель для простого "answer" от signaling сервера
|
||||
connectionManager.registerEventListener("answer", webrtcAnswerHandler)
|
||||
|
||||
// Обработка WebRTC ICE candidates
|
||||
val iceCandidateHandler: (Array<Any>) -> Unit = { data: Array<Any> ->
|
||||
try {
|
||||
if (data.isNotEmpty()) {
|
||||
val requestData = data[0] as? JSONObject
|
||||
requestData?.let {
|
||||
val sessionId = it.optString("sessionId")
|
||||
val candidate = it.optString("candidate")
|
||||
if (sessionId.isNotEmpty() && candidate.isNotEmpty()) {
|
||||
Log.d(TAG, "Received ICE candidate for session: $sessionId")
|
||||
connectionManager.handleWebRTCIceCandidate(sessionId, candidate)
|
||||
} else {
|
||||
Log.w(TAG, "Invalid ICE candidate data: sessionId=$sessionId, candidate length=${candidate.length}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing webrtc ice-candidate event", e)
|
||||
}
|
||||
}
|
||||
connectionManager.registerEventListener("webrtc:ice-candidate", iceCandidateHandler)
|
||||
connectionManager.registerEventListener("webrtc-ice-candidate", iceCandidateHandler)
|
||||
connectionManager.registerEventListener("ice:candidate", iceCandidateHandler) // Alternative format
|
||||
connectionManager.registerEventListener("ice-candidate", iceCandidateHandler) // Alternative format
|
||||
// Добавляем слушатель для простого "ice_candidate" от signaling сервера
|
||||
connectionManager.registerEventListener("ice_candidate", iceCandidateHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка запроса доступа к камере
|
||||
*/
|
||||
private fun handleCameraRequest(data: JSONObject) {
|
||||
// Проверяем, что JSON не пустой
|
||||
if (data.length() == 0) {
|
||||
Log.e(TAG, "Empty JSON object received for camera request")
|
||||
return
|
||||
}
|
||||
|
||||
val sessionId = data.optString("sessionId")
|
||||
val operatorId = data.optString("operatorId")
|
||||
val cameraType = data.optString("cameraType", "back")
|
||||
|
||||
// Расширенная валидация входных данных
|
||||
when {
|
||||
sessionId.isBlank() -> {
|
||||
Log.e(TAG, "Invalid camera request: sessionId is empty or blank")
|
||||
return
|
||||
}
|
||||
operatorId.isBlank() -> {
|
||||
Log.e(TAG, "Invalid camera request: operatorId is empty or blank")
|
||||
return
|
||||
}
|
||||
cameraType.isBlank() -> {
|
||||
Log.e(TAG, "Invalid camera request: cameraType is empty or blank, using default 'back'")
|
||||
// Не возвращаемся, а используем значение по умолчанию
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, нет ли уже активной сессии с этим sessionId
|
||||
if (_activeSessions.value.any { it.sessionId == sessionId }) {
|
||||
Log.w(TAG, "Session $sessionId already exists, ignoring duplicate request")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Processing camera request - Session: $sessionId, Operator: $operatorId, Camera: $cameraType")
|
||||
Log.d(TAG, "Auto approve setting: ${preferenceManager.getAutoApprove()}")
|
||||
|
||||
if (preferenceManager.getAutoApprove()) {
|
||||
// Автоматическое подтверждение включено
|
||||
Log.d(TAG, "Auto approve is ENABLED - automatically approving request")
|
||||
approveRequest(sessionId, operatorId, cameraType, isAutoApproved = true)
|
||||
} else {
|
||||
// Показать запрос пользователю
|
||||
Log.d(TAG, "Auto approve is DISABLED - showing notification to user")
|
||||
_pendingRequest.value = data
|
||||
showRequestNotification(operatorId, cameraType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подтверждение запроса доступа к камере
|
||||
*/
|
||||
fun approveRequest(sessionId: String, operatorId: String, cameraType: String, isAutoApproved: Boolean = false) {
|
||||
scope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Approving camera request - Session: $sessionId")
|
||||
|
||||
// Валидация входных данных
|
||||
if (sessionId.isEmpty() || operatorId.isEmpty()) {
|
||||
Log.e(TAG, "Cannot approve request: invalid parameters (sessionId=$sessionId, operatorId=$operatorId)")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Проверяем, нет ли уже активной сессии с этим sessionId
|
||||
if (_activeSessions.value.any { it.sessionId == sessionId }) {
|
||||
Log.w(TAG, "Session $sessionId already exists, cannot approve duplicate")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Отправляем подтверждение на сервер
|
||||
connectionManager.sendCameraResponse(sessionId, true, "", operatorId)
|
||||
|
||||
// Добавляем сессию в активные
|
||||
val session = CameraSession(sessionId, operatorId, cameraType, isAutoApproved = isAutoApproved, deviceId = android.provider.Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
android.provider.Settings.Secure.ANDROID_ID
|
||||
))
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
currentSessions.add(session)
|
||||
_activeSessions.value = currentSessions
|
||||
|
||||
// Запускаем стриминг через ConnectionManager (не через WebRTC напрямую)
|
||||
connectionManager.startVideoStream(sessionId, operatorId, cameraType)
|
||||
|
||||
// Очищаем pending запрос
|
||||
_pendingRequest.value = null
|
||||
|
||||
// Показываем уведомление
|
||||
showActiveStreamingNotification(operatorId, cameraType)
|
||||
|
||||
Log.d(TAG, "Camera request approved and streaming started for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to approve camera request for session: $sessionId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение запроса доступа к камере
|
||||
*/
|
||||
fun denyRequest(sessionId: String) {
|
||||
scope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Denying camera request - Session: $sessionId")
|
||||
|
||||
// Проверяем, что sessionId не пустой
|
||||
if (sessionId.isEmpty()) {
|
||||
Log.e(TAG, "Cannot deny request: sessionId is empty")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Получаем информацию о запросе из pending request
|
||||
val pendingRequestData = _pendingRequest.value
|
||||
val operatorId = pendingRequestData?.optString("operatorId") ?: ""
|
||||
|
||||
// Отправляем отклонение на сервер
|
||||
connectionManager.sendCameraResponse(sessionId, false, "User denied request", operatorId)
|
||||
|
||||
// Очищаем pending запрос
|
||||
_pendingRequest.value = null
|
||||
|
||||
Log.d(TAG, "Camera request denied for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to deny camera request for session: $sessionId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка отключения камеры
|
||||
*/
|
||||
private fun handleCameraDisconnect(data: JSONObject) {
|
||||
val sessionId = data.optString("sessionId")
|
||||
|
||||
Log.d(TAG, "Disconnecting camera session: $sessionId")
|
||||
|
||||
// Остановка WebRTC стриминга через ConnectionManager
|
||||
connectionManager.stopVideoStream(sessionId)
|
||||
|
||||
// Удаление сессии из активных
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
currentSessions.removeAll { it.sessionId == sessionId }
|
||||
_activeSessions.value = currentSessions
|
||||
|
||||
// Обновление уведомления
|
||||
if (currentSessions.isEmpty()) {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
} else {
|
||||
updateActiveStreamingNotification(currentSessions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание канала уведомлений
|
||||
*/
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
"Запросы доступа к камере",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
description = "Уведомления о запросах операторов на доступ к камере"
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать уведомление о запросе
|
||||
*/
|
||||
private fun showRequestNotification(operatorId: String, cameraType: String) {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_camera_request)
|
||||
.setContentTitle("Запрос доступа к камере")
|
||||
.setContentText("Оператор $operatorId запрашивает доступ к ${getCameraDisplayName(cameraType)}")
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(false)
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText("Оператор с UUID: $operatorId запрашивает доступ к ${getCameraDisplayName(cameraType)}"))
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать уведомление об активном стриминге
|
||||
*/
|
||||
private fun showActiveStreamingNotification(operatorId: String, cameraType: String) {
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_camera_active)
|
||||
.setContentTitle("Камера активна")
|
||||
.setContentText("Оператор $operatorId подключен к ${getCameraDisplayName(cameraType)}")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText("Оператор с UUID: $operatorId подключен к ${getCameraDisplayName(cameraType)}"))
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun updateActiveStreamingNotification(sessions: List<CameraSession>) {
|
||||
val operatorIds = sessions.map { it.operatorId }.distinct()
|
||||
val operatorText = if (operatorIds.size == 1) {
|
||||
"UUID: ${operatorIds.first()}"
|
||||
} else {
|
||||
"Операторы: ${operatorIds.size} подключено"
|
||||
}
|
||||
|
||||
val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_camera_active)
|
||||
.setContentTitle("Камера активна")
|
||||
.setContentText(operatorText)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText("Активные операторы:\n${operatorIds.joinToString("\n") { "UUID: $it" }}"))
|
||||
.build()
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun getCameraDisplayName(cameraType: String): String {
|
||||
return when (cameraType) {
|
||||
"back" -> "основной камере"
|
||||
"front" -> "фронтальной камере"
|
||||
"wide" -> "широкоугольной камере"
|
||||
"telephoto" -> "телеобъективу"
|
||||
else -> "камере"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка автоматически одобренного запроса
|
||||
*/
|
||||
private fun handleAutoApprovedRequest(data: JSONObject) {
|
||||
val sessionId = data.optString("sessionId")
|
||||
val operatorId = data.optString("operatorId")
|
||||
val cameraType = data.optString("cameraType", "back")
|
||||
|
||||
approveRequest(sessionId, operatorId, cameraType, isAutoApproved = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительное отключение сессии
|
||||
*/
|
||||
fun disconnectSession(sessionId: String) {
|
||||
scope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Forcibly disconnecting session: $sessionId")
|
||||
|
||||
// Остановка видеопотока
|
||||
connectionManager.stopVideoStream(sessionId)
|
||||
|
||||
// Удаление из активных сессий
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
val removedSession = currentSessions.find { it.sessionId == sessionId }
|
||||
currentSessions.removeAll { it.sessionId == sessionId }
|
||||
_activeSessions.value = currentSessions
|
||||
|
||||
// Обновление уведомлений
|
||||
if (currentSessions.isEmpty()) {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
} else {
|
||||
updateActiveStreamingNotification(currentSessions)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Session $sessionId disconnected successfully")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to disconnect session: $sessionId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение всех активных сессий
|
||||
*/
|
||||
fun disconnectAllSessions() {
|
||||
scope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Disconnecting all active sessions")
|
||||
|
||||
val currentSessions = _activeSessions.value.toList()
|
||||
|
||||
// Остановка всех видеопотоков
|
||||
currentSessions.forEach { session ->
|
||||
connectionManager.stopVideoStream(session.sessionId)
|
||||
}
|
||||
|
||||
// Очистка активных сессий
|
||||
_activeSessions.value = emptyList()
|
||||
|
||||
// Отмена уведомлений
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
|
||||
Log.d(TAG, "All sessions disconnected (${currentSessions.size} sessions)")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to disconnect all sessions", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о сессии
|
||||
*/
|
||||
fun getSessionInfo(sessionId: String): CameraSession? {
|
||||
return _activeSessions.value.find { it.sessionId == sessionId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение количества активных сессий
|
||||
*/
|
||||
fun getActiveSessionCount(): Int {
|
||||
return _activeSessions.value.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, есть ли активная сессия с данным оператором
|
||||
*/
|
||||
fun hasActiveSessionWithOperator(operatorId: String): Boolean {
|
||||
return _activeSessions.value.any { it.operatorId == operatorId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка ресурсов
|
||||
*/
|
||||
fun cleanup() {
|
||||
scope.cancel()
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
// Очистка активных сессий при закрытии
|
||||
_activeSessions.value = emptyList()
|
||||
_pendingRequest.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление состояния потоковой передачи сессии
|
||||
*/
|
||||
fun updateSessionStreamingState(sessionId: String, state: StreamingState) {
|
||||
scope.launch {
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
|
||||
|
||||
if (sessionIndex != -1) {
|
||||
val updatedSession = currentSessions[sessionIndex].copy(
|
||||
streamingState = state,
|
||||
lastHeartbeat = System.currentTimeMillis()
|
||||
)
|
||||
currentSessions[sessionIndex] = updatedSession
|
||||
_activeSessions.value = currentSessions
|
||||
|
||||
Log.d(TAG, "Updated session $sessionId state to $state")
|
||||
|
||||
// Обновляем общее состояние системы
|
||||
updateSystemState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление общего состояния системы GodEye Signal Center
|
||||
*/
|
||||
private fun updateSystemState() {
|
||||
val sessions = _activeSessions.value
|
||||
val newState = when {
|
||||
sessions.isEmpty() -> SystemState.READY
|
||||
sessions.any { it.streamingState == StreamingState.ERROR } -> SystemState.ERROR
|
||||
sessions.any { it.streamingState == StreamingState.STREAMING } -> SystemState.STREAMING_ACTIVE
|
||||
sessions.all { it.streamingState == StreamingState.INITIALIZING || it.streamingState == StreamingState.CONNECTING } ->
|
||||
SystemState.PROCESSING_REQUEST
|
||||
else -> SystemState.READY
|
||||
}
|
||||
|
||||
if (_systemState.value != newState) {
|
||||
_systemState.value = newState
|
||||
Log.d(TAG, "System state updated to: $newState")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение диагностической информации для архитектуры GodEye Signal Center
|
||||
*/
|
||||
fun getDiagnosticInfo(): JSONObject {
|
||||
return JSONObject().apply {
|
||||
put("systemState", _systemState.value.name)
|
||||
put("activeSessions", _activeSessions.value.size)
|
||||
put("autoApproveEnabled", preferenceManager.getAutoApprove())
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
|
||||
// Детальная информация о сессиях
|
||||
val sessionsArray = org.json.JSONArray()
|
||||
_activeSessions.value.forEach { session ->
|
||||
val sessionInfo = JSONObject().apply {
|
||||
put("sessionId", session.sessionId)
|
||||
put("operatorId", session.operatorId)
|
||||
put("cameraType", session.cameraType)
|
||||
put("isAutoApproved", session.isAutoApproved)
|
||||
put("streamingState", session.streamingState.name)
|
||||
put("duration", System.currentTimeMillis() - session.startTime)
|
||||
put("lastHeartbeat", session.lastHeartbeat)
|
||||
put("deviceId", session.deviceId)
|
||||
put("quality", session.quality)
|
||||
}
|
||||
sessionsArray.put(sessionInfo)
|
||||
}
|
||||
put("sessions", sessionsArray)
|
||||
|
||||
// Статистика для мониторинга
|
||||
val stats = JSONObject().apply {
|
||||
val streamingCount = _activeSessions.value.count { it.streamingState == StreamingState.STREAMING }
|
||||
val errorCount = _activeSessions.value.count { it.streamingState == StreamingState.ERROR }
|
||||
val connectingCount = _activeSessions.value.count { it.streamingState == StreamingState.CONNECTING }
|
||||
|
||||
put("streamingSessions", streamingCount)
|
||||
put("errorSessions", errorCount)
|
||||
put("connectingSessions", connectingCount)
|
||||
put("autoApprovedSessions", _activeSessions.value.count { it.isAutoApproved })
|
||||
}
|
||||
put("statistics", stats)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка "мертвых" сессий и их очистка
|
||||
* Метод для поддержания надежности системы GodEye Signal Center
|
||||
*/
|
||||
fun performHealthCheck() {
|
||||
scope.launch {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val staleThreshold = 30000L // 30 секунд без heartbeat
|
||||
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
val staleSessions = currentSessions.filter { session ->
|
||||
currentTime - session.lastHeartbeat > staleThreshold &&
|
||||
session.streamingState != StreamingState.STREAMING
|
||||
}
|
||||
|
||||
if (staleSessions.isNotEmpty()) {
|
||||
Log.w(TAG, "Found ${staleSessions.size} stale sessions, cleaning up...")
|
||||
|
||||
staleSessions.forEach { session ->
|
||||
Log.w(TAG, "Removing stale session: ${session.sessionId}")
|
||||
connectionManager.stopVideoStream(session.sessionId)
|
||||
currentSessions.remove(session)
|
||||
}
|
||||
|
||||
_activeSessions.value = currentSessions
|
||||
updateSystemState()
|
||||
|
||||
// Обновляем уведомления
|
||||
if (currentSessions.isEmpty()) {
|
||||
notificationManager.cancel(NOTIFICATION_ID)
|
||||
} else {
|
||||
updateActiveStreamingNotification(currentSessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка heartbeat для активных сессий
|
||||
* Поддерживает мониторинг состояния в архитектуре GodEye Signal Center
|
||||
*/
|
||||
fun sendSessionHeartbeat(sessionId: String) {
|
||||
scope.launch {
|
||||
val currentSessions = _activeSessions.value.toMutableList()
|
||||
val sessionIndex = currentSessions.indexOfFirst { it.sessionId == sessionId }
|
||||
|
||||
if (sessionIndex != -1) {
|
||||
val updatedSession = currentSessions[sessionIndex].copy(
|
||||
lastHeartbeat = System.currentTimeMillis()
|
||||
)
|
||||
currentSessions[sessionIndex] = updatedSession
|
||||
_activeSessions.value = currentSessions
|
||||
|
||||
// Отправляем heartbeat на сервер через ConnectionManager
|
||||
try {
|
||||
val heartbeatData = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
put("streamingState", updatedSession.streamingState.name)
|
||||
put("deviceId", updatedSession.deviceId)
|
||||
}
|
||||
|
||||
// Используем методы ConnectionManager для отправки heartbeat
|
||||
// Пока не создадим специальный метод, логируем для отладки
|
||||
Log.d(TAG, "Sending session heartbeat for $sessionId: $heartbeatData")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send heartbeat for session $sessionId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение метрик производительности для мониторинга GodEye Signal Center
|
||||
*/
|
||||
fun getPerformanceMetrics(): JSONObject {
|
||||
val sessions = _activeSessions.value
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
return JSONObject().apply {
|
||||
// Общие метрики
|
||||
put("totalSessions", sessions.size)
|
||||
put("systemUptime", currentTime - startTime)
|
||||
put("averageSessionDuration", if (sessions.isNotEmpty()) {
|
||||
sessions.map { currentTime - it.startTime }.average()
|
||||
} else 0.0)
|
||||
|
||||
// Метрики по состоянию
|
||||
StreamingState.values().forEach { state ->
|
||||
put("sessions_${state.name.lowercase()}", sessions.count { it.streamingState == state })
|
||||
}
|
||||
|
||||
// Метрики по типам камер
|
||||
val cameraTypes = sessions.groupBy { it.cameraType }
|
||||
cameraTypes.forEach { (type, typeSessions) ->
|
||||
put("camera_${type}_sessions", typeSessions.size)
|
||||
}
|
||||
|
||||
// Метрики автоодобрения
|
||||
put("autoApprovedSessions", sessions.count { it.isAutoApproved })
|
||||
put("manualApprovedSessions", sessions.count { !it.isAutoApproved })
|
||||
|
||||
put("timestamp", currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
private val startTime = System.currentTimeMillis()
|
||||
|
||||
/**
|
||||
* Генерация UUID для новой сессии
|
||||
*/
|
||||
fun generateSessionId(): String {
|
||||
return java.util.UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Тестовая функция для проверки автоподтверждения с UUID
|
||||
*/
|
||||
fun testAutoApprovalWithUUID() {
|
||||
try {
|
||||
val testSessionId = generateSessionId()
|
||||
val testOperatorId = java.util.UUID.randomUUID().toString()
|
||||
|
||||
Log.d(TAG, "Testing auto approval with UUID session ID: $testSessionId")
|
||||
|
||||
// Создаем фиктивный запрос камеры с UUID
|
||||
val testRequest = JSONObject().apply {
|
||||
put("sessionId", testSessionId)
|
||||
put("operatorId", testOperatorId)
|
||||
put("cameraType", "back")
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
}
|
||||
|
||||
Log.d(TAG, "Simulating camera request with UUID: $testRequest")
|
||||
|
||||
// Обрабатываем запрос
|
||||
handleCameraRequest(testRequest)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to test auto approval with UUID", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,7 @@ class Camera2Manager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Используем стандартный метод createCaptureSession вместо устаревшего
|
||||
camera.createCaptureSession(listOf(surface), sessionCallback, null)
|
||||
|
||||
} catch (e: CameraAccessException) {
|
||||
|
||||
1237
app/src/main/java/com/example/godeye/managers/ConnectionManager.kt
Normal file
1237
app/src/main/java/com/example/godeye/managers/ConnectionManager.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,10 +34,10 @@ class PermissionManager(private val context: Context) {
|
||||
Manifest.permission.WAKE_LOCK,
|
||||
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)
|
||||
}
|
||||
// Добавляем FOREGROUND_SERVICE_CAMERA для API 34+ (но в Android 10 недоступно)
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
// plus(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +193,7 @@ class PermissionManager(private val context: Context) {
|
||||
Manifest.permission.ACCESS_NETWORK_STATE -> "Проверка состояния сети"
|
||||
Manifest.permission.WAKE_LOCK -> "Предотвращение засыпания устройства"
|
||||
Manifest.permission.FOREGROUND_SERVICE -> "Работа в фоновом режиме"
|
||||
Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой"
|
||||
// Manifest.permission.FOREGROUND_SERVICE_CAMERA -> "Фоновая работа с камерой" // Недоступно в Android 10
|
||||
else -> "Системное разрешение"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +1,35 @@
|
||||
package com.example.godeye.managers
|
||||
|
||||
import com.example.godeye.models.*
|
||||
import com.example.godeye.utils.Logger
|
||||
import com.example.godeye.models.CameraSession
|
||||
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 = ConcurrentHashMap<String, CameraSession>()
|
||||
|
||||
private val _sessions = MutableStateFlow<Map<String, CameraSession>>(emptyMap())
|
||||
val sessions: StateFlow<Map<String, CameraSession>> = _sessions.asStateFlow()
|
||||
|
||||
private val _activeSessionCount = MutableStateFlow(0)
|
||||
val activeSessionCount: StateFlow<Int> = _activeSessionCount.asStateFlow()
|
||||
|
||||
/**
|
||||
* Создание новой сессии при принятии запроса оператора
|
||||
*/
|
||||
fun createSession(sessionId: String, operatorId: String, cameraType: String): CameraSession {
|
||||
Logger.step("SESSION_CREATE", "Creating session: $sessionId for operator $operatorId")
|
||||
|
||||
fun createSession(sessionId: String, operatorId: String, cameraType: String) {
|
||||
val session = CameraSession(
|
||||
sessionId = sessionId,
|
||||
operatorId = operatorId,
|
||||
cameraType = cameraType,
|
||||
startTime = System.currentTimeMillis(),
|
||||
isActive = true,
|
||||
webRTCConnected = false
|
||||
startTime = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
activeSessions[sessionId] = session
|
||||
updateSessionsFlow()
|
||||
|
||||
Logger.step("SESSION_CREATED", "Session created: $sessionId")
|
||||
return session
|
||||
val currentSessions = _sessions.value.toMutableMap()
|
||||
currentSessions[sessionId] = session
|
||||
_sessions.value = currentSessions
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление статуса WebRTC соединения для сессии
|
||||
*/
|
||||
fun updateWebRTCConnection(sessionId: String, connected: Boolean) {
|
||||
activeSessions[sessionId]?.let { session ->
|
||||
session.webRTCConnected = connected
|
||||
activeSessions[sessionId] = session
|
||||
updateSessionsFlow()
|
||||
|
||||
Logger.step("SESSION_WEBRTC_UPDATED",
|
||||
"Session $sessionId WebRTC status updated: $connected")
|
||||
}
|
||||
fun endSession(sessionId: String, reason: String = "Session ended") {
|
||||
val currentSessions = _sessions.value.toMutableMap()
|
||||
currentSessions.remove(sessionId)
|
||||
_sessions.value = currentSessions
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение сессии
|
||||
*/
|
||||
fun endSession(sessionId: String, reason: String = "User ended") {
|
||||
activeSessions[sessionId]?.let { session ->
|
||||
session.isActive = false
|
||||
activeSessions.remove(sessionId)
|
||||
updateSessionsFlow()
|
||||
|
||||
Logger.step("SESSION_ENDED", "Session ended: $sessionId, reason: $reason")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение активной сессии по ID
|
||||
*/
|
||||
fun getSession(sessionId: String): CameraSession? {
|
||||
return activeSessions[sessionId]
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех активных сессий
|
||||
*/
|
||||
fun getAllActiveSessions(): List<CameraSession> {
|
||||
return activeSessions.values.filter { it.isActive }
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, есть ли активные сессии
|
||||
*/
|
||||
fun hasActiveSessions(): Boolean {
|
||||
return activeSessions.values.any { it.isActive }
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение всех активных сессий
|
||||
*/
|
||||
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 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 getSessionStats(): SessionStats {
|
||||
val active = activeSessions.values.filter { it.isActive }
|
||||
val withWebRTC = active.filter { it.webRTCConnected }
|
||||
|
||||
return SessionStats(
|
||||
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 }
|
||||
fun endAllSessions(reason: String = "All sessions ended") {
|
||||
_sessions.value = emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Статистика сессий
|
||||
*/
|
||||
data class SessionStats(
|
||||
val totalActive: Int,
|
||||
val webRTCConnected: Int,
|
||||
val operators: Int,
|
||||
val averageDuration: Long
|
||||
)
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.example.godeye.managers
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.example.godeye.models.*
|
||||
import com.example.godeye.signaling.SignalingClient
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Менеджер сигналлинга для управления подключениями к операторам
|
||||
*/
|
||||
class SignalingManager(
|
||||
private val context: Context
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "SignalingManager"
|
||||
}
|
||||
|
||||
private val signalingClient = SignalingClient()
|
||||
private var managerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
// Состояния
|
||||
val signalingState: StateFlow<SignalingState> = signalingClient.signalingState
|
||||
val sessionState: StateFlow<SessionState> = signalingClient.sessionState
|
||||
val currentCall: StateFlow<IncomingCall?> = signalingClient.currentCall
|
||||
|
||||
// Callbacks для WebRTC интеграции
|
||||
private var webRTCManager: Any? = null // Будет связан с WebRTCManager
|
||||
|
||||
/**
|
||||
* Запуск сигналлинга
|
||||
*/
|
||||
fun start(serverUrl: String? = null) {
|
||||
Log.d(TAG, "Starting signaling manager")
|
||||
|
||||
managerScope.launch {
|
||||
try {
|
||||
val deviceInfo = createDeviceInfo()
|
||||
val url = serverUrl ?: getDefaultServerUrl()
|
||||
|
||||
Log.d(TAG, "Connecting to signaling server: $url")
|
||||
signalingClient.connect(url, deviceInfo)
|
||||
|
||||
// Настраиваем обработчики WebRTC сигналлинга
|
||||
setupWebRTCHandlers()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start signaling", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка сигналлинга
|
||||
*/
|
||||
fun stop() {
|
||||
Log.d(TAG, "Stopping signaling manager")
|
||||
|
||||
signalingClient.disconnect()
|
||||
managerScope.cancel()
|
||||
managerScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
}
|
||||
|
||||
/**
|
||||
* Принятие входящего вызова
|
||||
*/
|
||||
fun acceptIncomingCall() {
|
||||
Log.d(TAG, "Accepting incoming call")
|
||||
signalingClient.acceptCall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение входящего вызова
|
||||
*/
|
||||
fun rejectIncomingCall() {
|
||||
Log.d(TAG, "Rejecting incoming call")
|
||||
signalingClient.rejectCall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение активного вызова
|
||||
*/
|
||||
fun endActiveCall() {
|
||||
Log.d(TAG, "Ending active call")
|
||||
signalingClient.endCall()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание информации об устройстве
|
||||
*/
|
||||
private fun createDeviceInfo(): DeviceInfo {
|
||||
return DeviceInfo(
|
||||
deviceId = getDeviceId(),
|
||||
deviceName = android.os.Build.MODEL,
|
||||
androidVersion = android.os.Build.VERSION.RELEASE,
|
||||
appVersion = "1.0"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение уникального ID устройства
|
||||
*/
|
||||
private fun getDeviceId(): String {
|
||||
return try {
|
||||
Settings.Secure.getString(
|
||||
context.contentResolver,
|
||||
Settings.Secure.ANDROID_ID
|
||||
) ?: "unknown_device_${System.currentTimeMillis()}"
|
||||
} catch (e: Exception) {
|
||||
"unknown_device_${System.currentTimeMillis()}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение имени устройства
|
||||
*/
|
||||
private fun getDeviceName(): String {
|
||||
return try {
|
||||
"${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}"
|
||||
} catch (e: Exception) {
|
||||
"Android Device"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение URL сервера по умолчанию
|
||||
*/
|
||||
private fun getDefaultServerUrl(): String {
|
||||
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
|
||||
return "ws://192.168.219.108:3000"
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков WebRTC сигналлинга
|
||||
*/
|
||||
private fun setupWebRTCHandlers() {
|
||||
signalingClient.setOnOfferReceived { offer ->
|
||||
Log.d(TAG, "Received WebRTC offer")
|
||||
// TODO: Передать offer в WebRTCManager
|
||||
handleWebRTCOffer(offer)
|
||||
}
|
||||
|
||||
signalingClient.setOnAnswerReceived { answer ->
|
||||
Log.d(TAG, "Received WebRTC answer")
|
||||
// TODO: Передать answer в WebRTCManager
|
||||
handleWebRTCAnswer(answer)
|
||||
}
|
||||
|
||||
signalingClient.setOnIceCandidateReceived { candidate ->
|
||||
Log.d(TAG, "Received ICE candidate")
|
||||
// TODO: Передать candidate в WebRTCManager
|
||||
handleICECandidate(candidate)
|
||||
}
|
||||
|
||||
signalingClient.setOnCallEnded {
|
||||
Log.d(TAG, "Call ended by operator")
|
||||
handleCallEnded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка WebRTC Offer
|
||||
*/
|
||||
private fun handleWebRTCOffer(offer: RTCSessionDescription) {
|
||||
managerScope.launch {
|
||||
try {
|
||||
// TODO: Интеграция с WebRTCManager
|
||||
Log.d(TAG, "Processing WebRTC offer: ${offer.type}")
|
||||
|
||||
// Здесь должна быть логика создания answer и его отправки
|
||||
// val answer = webRTCManager.createAnswer(offer)
|
||||
// signalingClient.sendAnswer(answer, currentCall.value?.operatorId ?: "")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing WebRTC offer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка WebRTC Answer
|
||||
*/
|
||||
private fun handleWebRTCAnswer(answer: RTCSessionDescription) {
|
||||
managerScope.launch {
|
||||
try {
|
||||
// TODO: Интеграция с WebRTCManager
|
||||
Log.d(TAG, "Processing WebRTC answer: ${answer.type}")
|
||||
|
||||
// webRTCManager.setRemoteDescription(answer)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing WebRTC answer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка ICE Candidate
|
||||
*/
|
||||
private fun handleICECandidate(candidate: RTCIceCandidate) {
|
||||
managerScope.launch {
|
||||
try {
|
||||
// TODO: Интеграция с WebRTCManager
|
||||
Log.d(TAG, "Processing ICE candidate")
|
||||
|
||||
// webRTCManager.addIceCandidate(candidate)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing ICE candidate", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка завершения вызова
|
||||
*/
|
||||
private fun handleCallEnded() {
|
||||
managerScope.launch {
|
||||
try {
|
||||
// TODO: Очистка WebRTC соединения
|
||||
Log.d(TAG, "Cleaning up after call end")
|
||||
|
||||
// webRTCManager.cleanup()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error cleaning up after call end", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка WebRTC Offer (для исходящих вызовов)
|
||||
*/
|
||||
fun sendOffer(offer: RTCSessionDescription) {
|
||||
val operatorId = currentCall.value?.operatorId ?: return
|
||||
signalingClient.sendOffer(offer, operatorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка WebRTC Answer
|
||||
*/
|
||||
fun sendAnswer(answer: RTCSessionDescription) {
|
||||
val operatorId = currentCall.value?.operatorId ?: return
|
||||
signalingClient.sendAnswer(answer, operatorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка ICE Candidate
|
||||
*/
|
||||
fun sendIceCandidate(candidate: RTCIceCandidate) {
|
||||
val operatorId = currentCall.value?.operatorId ?: return
|
||||
signalingClient.sendIceCandidate(candidate, operatorId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о текущем состоянии
|
||||
*/
|
||||
fun getConnectionInfo(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"signalingState" to signalingState.value.name,
|
||||
"sessionState" to sessionState.value.name,
|
||||
"hasActiveCall" to (currentCall.value != null),
|
||||
"operatorId" to (currentCall.value?.operatorId ?: "none"),
|
||||
"deviceId" to getDeviceId()
|
||||
)
|
||||
}
|
||||
}
|
||||
308
app/src/main/java/com/example/godeye/managers/SocketManager.kt
Normal file
308
app/src/main/java/com/example/godeye/managers/SocketManager.kt
Normal file
@@ -0,0 +1,308 @@
|
||||
package com.example.godeye.managers
|
||||
|
||||
import android.content.Context
|
||||
import com.example.godeye.utils.Logger
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import io.socket.client.IO
|
||||
import io.socket.client.Socket
|
||||
import org.json.JSONObject
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Менеджер Socket.IO соединения с сервером
|
||||
*/
|
||||
class SocketManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SocketManager"
|
||||
}
|
||||
|
||||
// События подключения
|
||||
sealed class ConnectionEvent {
|
||||
object Connected : ConnectionEvent()
|
||||
object Disconnected : ConnectionEvent()
|
||||
data class Error(val message: String) : ConnectionEvent()
|
||||
data class CameraRequest(val sessionId: String, val operatorId: String, val message: String) : ConnectionEvent()
|
||||
}
|
||||
|
||||
private val _events = MutableStateFlow<ConnectionEvent?>(null)
|
||||
val events: Flow<ConnectionEvent?> = _events
|
||||
|
||||
private var socket: Socket? = null
|
||||
private var webRTCManager: WebRTCManager? = null
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Добавляем флаг для предотвращения множественной регистрации
|
||||
private var isDeviceRegistered = false
|
||||
private var currentDeviceId: String? = null
|
||||
|
||||
/**
|
||||
* Подключение к серверу
|
||||
*/
|
||||
fun connect(serverUrl: String, webRTCManager: WebRTCManager) {
|
||||
try {
|
||||
this.webRTCManager = webRTCManager
|
||||
|
||||
Logger.step("SOCKET_CONNECTING", "Connecting to server: $serverUrl")
|
||||
|
||||
val options = IO.Options().apply {
|
||||
forceNew = true
|
||||
reconnection = true
|
||||
timeout = 10000
|
||||
}
|
||||
|
||||
socket = IO.socket(URI.create(serverUrl), options)
|
||||
|
||||
setupConnectionHandlers()
|
||||
setupCameraHandlers()
|
||||
setupWebRTCHandlers()
|
||||
setupWebRTCCallbacks()
|
||||
|
||||
socket?.connect()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_CONNECT_ERROR", "Failed to connect to server", e)
|
||||
_events.value = ConnectionEvent.Error("Ошибка подключения: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка основных обработчиков подключения
|
||||
*/
|
||||
private fun setupConnectionHandlers() {
|
||||
socket?.on(Socket.EVENT_CONNECT) {
|
||||
Logger.step("SOCKET_CONNECTED", "Connected to server")
|
||||
_events.value = ConnectionEvent.Connected
|
||||
}
|
||||
|
||||
socket?.on(Socket.EVENT_DISCONNECT) {
|
||||
Logger.step("SOCKET_DISCONNECTED", "Disconnected from server")
|
||||
_events.value = ConnectionEvent.Disconnected
|
||||
}
|
||||
|
||||
socket?.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)
|
||||
_events.value = ConnectionEvent.Error("Ошибка подключения: $error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков камеры
|
||||
*/
|
||||
private fun setupCameraHandlers() {
|
||||
socket?.on("camera_request") { args ->
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val operatorId = data.getString("operatorId")
|
||||
val message = data.getString("message")
|
||||
|
||||
Logger.step("SOCKET_CAMERA_REQUEST", "Camera request received from operator: $operatorId")
|
||||
_events.value = ConnectionEvent.CameraRequest(sessionId, operatorId, message)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_CAMERA_REQUEST_ERROR", "Failed to handle camera request", e)
|
||||
}
|
||||
}
|
||||
|
||||
socket?.on("stop_camera") { args ->
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
|
||||
Logger.step("SOCKET_STOP_CAMERA", "Stop camera request for session: $sessionId")
|
||||
webRTCManager?.stopStreaming(sessionId)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_STOP_CAMERA_ERROR", "Failed to handle stop camera", e)
|
||||
}
|
||||
}
|
||||
|
||||
socket?.on("switch_camera") { args ->
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
|
||||
Logger.step("SOCKET_SWITCH_CAMERA", "Switch camera request for session: $sessionId")
|
||||
webRTCManager?.switchCamera()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_SWITCH_CAMERA_ERROR", "Failed to handle switch camera", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков WebRTC сигналов
|
||||
*/
|
||||
private fun setupWebRTCHandlers() {
|
||||
socket?.on("webrtc_answer") { args ->
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val answerSdp = data.getString("answer")
|
||||
|
||||
Logger.step("SOCKET_WEBRTC_ANSWER", "Received WebRTC answer for session: $sessionId")
|
||||
webRTCManager?.handleAnswer(sessionId, answerSdp)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_WEBRTC_ANSWER_ERROR", "Failed to handle WebRTC answer", e)
|
||||
}
|
||||
}
|
||||
|
||||
socket?.on("webrtc_ice_candidate") { args ->
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val sessionId = data.getString("sessionId")
|
||||
val candidateJson = data.getString("candidate")
|
||||
|
||||
Logger.step("SOCKET_WEBRTC_ICE", "Received ICE candidate for session: $sessionId")
|
||||
webRTCManager?.handleIceCandidate(sessionId, candidateJson)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_WEBRTC_ICE_ERROR", "Failed to handle ICE candidate", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка колбэков WebRTC для отправки сигналов на сервер
|
||||
*/
|
||||
private fun setupWebRTCCallbacks() {
|
||||
webRTCManager?.onOfferCreated = { sessionId, offer ->
|
||||
scope.launch {
|
||||
try {
|
||||
// ИСПРАВЛЕНО: Отправляем offer в правильном формате для вашего сервера
|
||||
val offerData = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
put("offer", offer) // Отправляем как строку SDP, не JSON объект
|
||||
}
|
||||
|
||||
socket?.emit("webrtc_offer", offerData)
|
||||
Logger.step("SOCKET_WEBRTC_OFFER_SENT", "WebRTC offer sent for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_WEBRTC_OFFER_ERROR", "Failed to send WebRTC offer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webRTCManager?.onIceCandidateCreated = { sessionId, candidate ->
|
||||
scope.launch {
|
||||
try {
|
||||
val candidateData = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
put("candidate", candidate)
|
||||
}
|
||||
|
||||
socket?.emit("webrtc_ice_candidate", candidateData)
|
||||
Logger.step("SOCKET_ICE_SENT", "ICE candidate sent for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_ICE_ERROR", "Failed to send ICE candidate", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация устройства на сервере
|
||||
*/
|
||||
fun registerDevice(deviceId: String, deviceName: String) {
|
||||
try {
|
||||
// ИСПРАВЛЕНИЕ: Проверяем, зарегистрировано ли уже устройство
|
||||
if (isDeviceRegistered && deviceId == currentDeviceId) {
|
||||
Logger.step("SOCKET_DEVICE_ALREADY_REGISTERED", "Device is already registered: $deviceName ($deviceId)")
|
||||
return
|
||||
}
|
||||
|
||||
val data = JSONObject().apply {
|
||||
put("deviceId", deviceId)
|
||||
put("deviceName", deviceName)
|
||||
}
|
||||
|
||||
socket?.emit("device_register", data)
|
||||
Logger.step("SOCKET_DEVICE_REGISTER", "Device registration sent: $deviceName ($deviceId)")
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Устанавливаем флаг регистрации устройства
|
||||
isDeviceRegistered = true
|
||||
currentDeviceId = deviceId
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_REGISTER_ERROR", "Failed to register device", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Подтверждение доступа к камере
|
||||
*/
|
||||
fun approveCameraAccess(sessionId: String) {
|
||||
try {
|
||||
val data = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
put("approved", true)
|
||||
}
|
||||
|
||||
socket?.emit("camera_approved", data)
|
||||
Logger.step("SOCKET_CAMERA_APPROVED", "Camera access approved for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_APPROVE_ERROR", "Failed to approve camera access", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление о запуске камеры
|
||||
*/
|
||||
fun notifyCameraStarted(sessionId: String) {
|
||||
try {
|
||||
val data = JSONObject().apply {
|
||||
put("sessionId", sessionId)
|
||||
}
|
||||
|
||||
socket?.emit("camera_started", data)
|
||||
Logger.step("SOCKET_CAMERA_STARTED", "Camera started notification sent for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_CAMERA_STARTED_ERROR", "Failed to send camera started notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение от сервера
|
||||
*/
|
||||
fun disconnect() {
|
||||
try {
|
||||
Logger.step("SOCKET_DISCONNECTING", "Disconnecting from server")
|
||||
socket?.disconnect()
|
||||
socket = null
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_DISCONNECT_ERROR", "Error during disconnect", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка состояния подключения
|
||||
*/
|
||||
fun isConnected(): Boolean {
|
||||
return socket?.connected() == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождение ресурсов
|
||||
*/
|
||||
fun dispose() {
|
||||
try {
|
||||
disconnect()
|
||||
scope.cancel()
|
||||
Logger.step("SOCKET_DISPOSED", "SocketManager disposed")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SOCKET_DISPOSE_ERROR", "Error during dispose", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
1234
app/src/main/java/com/example/godeye/managers/WebRTCManager.kt
Normal file
1234
app/src/main/java/com/example/godeye/managers/WebRTCManager.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
package com.example.godeye.managers
|
||||
|
||||
import com.example.godeye.utils.Logger
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Менеджер WebSocket соединений для сигналинга WebRTC
|
||||
*/
|
||||
class WebSocketManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WebSocketManager"
|
||||
private const val PING_INTERVAL = 30L // секунд
|
||||
private const val RECONNECT_INTERVAL = 5L // секунд
|
||||
private const val MAX_RECONNECT_ATTEMPTS = 5
|
||||
}
|
||||
|
||||
private var webSocket: WebSocket? = null
|
||||
private var client: OkHttpClient? = null
|
||||
private var isConnected = false
|
||||
private var reconnectAttempts = 0
|
||||
private var shouldReconnect = true
|
||||
private var currentUrl: String? = null
|
||||
|
||||
// Колбэки
|
||||
var onMessageReceived: ((String) -> Unit)? = null
|
||||
var onConnectionStateChanged: ((Boolean) -> Unit)? = null
|
||||
var onError: ((String) -> Unit)? = null
|
||||
|
||||
// Корутины
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var reconnectJob: Job? = null
|
||||
|
||||
init {
|
||||
initializeClient()
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация HTTP клиента
|
||||
*/
|
||||
private fun initializeClient() {
|
||||
client = OkHttpClient.Builder()
|
||||
.pingInterval(PING_INTERVAL, TimeUnit.SECONDS)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Подключение к WebSocket серверу
|
||||
*/
|
||||
fun connect(url: String) {
|
||||
try {
|
||||
Logger.step("WEBSOCKET_CONNECT", "Connecting to WebSocket: $url")
|
||||
|
||||
currentUrl = url
|
||||
shouldReconnect = true
|
||||
reconnectAttempts = 0
|
||||
|
||||
disconnect() // Отключаемся от предыдущего соединения
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
webSocket = client?.newWebSocket(request, object : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
Logger.step("WEBSOCKET_OPENED", "WebSocket connection opened")
|
||||
isConnected = true
|
||||
reconnectAttempts = 0
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onConnectionStateChanged?.invoke(true)
|
||||
}
|
||||
|
||||
// Отправляем приветственное сообщение
|
||||
sendConnectionMessage()
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
Logger.step("WEBSOCKET_MESSAGE", "Message received: ${text.take(100)}...")
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onMessageReceived?.invoke(text)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Logger.step("WEBSOCKET_CLOSING", "WebSocket closing: $code - $reason")
|
||||
isConnected = false
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Logger.step("WEBSOCKET_CLOSED", "WebSocket closed: $code - $reason")
|
||||
isConnected = false
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
}
|
||||
|
||||
// Попытка переподключения
|
||||
if (shouldReconnect) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Logger.error("WEBSOCKET_ERROR", "WebSocket error: ${t.message}", t)
|
||||
isConnected = false
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
onError?.invoke("WebSocket ошибка: ${t.message}")
|
||||
}
|
||||
|
||||
// Попытка переподключения
|
||||
if (shouldReconnect) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBSOCKET_CONNECT_ERROR", "Failed to connect to WebSocket", e)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onError?.invoke("Ошибка подключения: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения
|
||||
*/
|
||||
fun sendMessage(message: String): Boolean {
|
||||
return try {
|
||||
if (isConnected && webSocket != null) {
|
||||
val success = webSocket!!.send(message)
|
||||
if (success) {
|
||||
Logger.step("WEBSOCKET_SEND", "Message sent: ${message.take(100)}...")
|
||||
} else {
|
||||
Logger.error("WEBSOCKET_SEND_ERROR", "Failed to send message", null)
|
||||
}
|
||||
success
|
||||
} else {
|
||||
Logger.error("WEBSOCKET_NOT_CONNECTED", "WebSocket not connected", null)
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBSOCKET_SEND_EXCEPTION", "Exception while sending message", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка JSON сообщения
|
||||
*/
|
||||
fun sendJsonMessage(json: JSONObject): Boolean {
|
||||
return sendMessage(json.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка приветственного сообщения
|
||||
*/
|
||||
private fun sendConnectionMessage() {
|
||||
try {
|
||||
val connectionMessage = JSONObject().apply {
|
||||
put("type", "device_connected")
|
||||
put("deviceType", "android")
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
put("capabilities", JSONObject().apply {
|
||||
put("video", true)
|
||||
put("audio", true)
|
||||
put("webrtc", true)
|
||||
})
|
||||
}
|
||||
|
||||
sendJsonMessage(connectionMessage)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBSOCKET_CONNECTION_MESSAGE_ERROR", "Failed to send connection message", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Планирование переподключения
|
||||
*/
|
||||
private fun scheduleReconnect() {
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
Logger.error("WEBSOCKET_MAX_RECONNECT", "Maximum reconnection attempts reached", null)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onError?.invoke("Превышено максимальное количество попыток переподключения")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
reconnectJob?.cancel()
|
||||
reconnectJob = scope.launch {
|
||||
delay(RECONNECT_INTERVAL * 1000)
|
||||
|
||||
if (shouldReconnect && !isConnected) {
|
||||
reconnectAttempts++
|
||||
Logger.step("WEBSOCKET_RECONNECT", "Reconnection attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS")
|
||||
|
||||
currentUrl?.let { url ->
|
||||
connect(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение от WebSocket
|
||||
*/
|
||||
fun disconnect() {
|
||||
try {
|
||||
Logger.step("WEBSOCKET_DISCONNECT", "Disconnecting WebSocket")
|
||||
|
||||
shouldReconnect = false
|
||||
reconnectJob?.cancel()
|
||||
|
||||
webSocket?.close(1000, "Client disconnect")
|
||||
webSocket = null
|
||||
isConnected = false
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
onConnectionStateChanged?.invoke(false)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBSOCKET_DISCONNECT_ERROR", "Failed to disconnect WebSocket", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка состояния соединения
|
||||
*/
|
||||
fun isConnected(): Boolean {
|
||||
return isConnected
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение URL текущего соединения
|
||||
*/
|
||||
fun getCurrentUrl(): String? {
|
||||
return currentUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождение ресурсов
|
||||
*/
|
||||
fun dispose() {
|
||||
try {
|
||||
Logger.step("WEBSOCKET_DISPOSE", "Disposing WebSocket manager")
|
||||
|
||||
disconnect()
|
||||
scope.cancel()
|
||||
|
||||
Logger.step("WEBSOCKET_DISPOSED", "WebSocket manager disposed")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBSOCKET_DISPOSE_ERROR", "Failed to dispose WebSocket manager", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/src/main/java/com/example/godeye/models/DeviceInfo.kt
Normal file
15
app/src/main/java/com/example/godeye/models/DeviceInfo.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.example.godeye.models
|
||||
|
||||
/**
|
||||
* Модель информации об устройстве
|
||||
*/
|
||||
data class DeviceInfo(
|
||||
val deviceId: String,
|
||||
val deviceName: String,
|
||||
val androidVersion: String,
|
||||
val appVersion: String,
|
||||
val availableCameras: List<String> = listOf("back", "front"),
|
||||
// Добавляем устаревшие поля для совместимости со старым кодом
|
||||
val model: String = deviceName,
|
||||
val manufacturer: String = android.os.Build.MANUFACTURER
|
||||
)
|
||||
@@ -53,3 +53,5 @@ object SocketEvents {
|
||||
const val HEARTBEAT_ACK = "heartbeat:ack"
|
||||
const val ERROR = "error"
|
||||
}
|
||||
|
||||
// DeviceInfo перенесен в отдельный файл DeviceInfo.kt
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.example.godeye.models
|
||||
|
||||
/**
|
||||
* Состояния подключения к сигналлинг серверу
|
||||
*/
|
||||
enum class SignalingState {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
RECONNECTING,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояния сессии с оператором
|
||||
*/
|
||||
enum class SessionState {
|
||||
WAITING, // Ожидание подключения оператора
|
||||
INCOMING, // Входящий вызов от оператора
|
||||
ACTIVE, // Активная сессия
|
||||
ENDED // Сессия завершена
|
||||
}
|
||||
|
||||
/**
|
||||
* Типы сообщений сигналлинга
|
||||
*/
|
||||
enum class SignalingMessageType {
|
||||
DEVICE_REGISTER,
|
||||
OPERATOR_CALL,
|
||||
CALL_ACCEPT,
|
||||
CALL_REJECT,
|
||||
OFFER,
|
||||
ANSWER,
|
||||
ICE_CANDIDATE,
|
||||
CALL_END
|
||||
}
|
||||
|
||||
/**
|
||||
* Базовое сообщение сигналлинга
|
||||
*/
|
||||
data class SignalingMessage(
|
||||
val type: SignalingMessageType,
|
||||
val from: String? = null,
|
||||
val to: String? = null,
|
||||
val data: Any? = null,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
/**
|
||||
* Данные входящего вызова
|
||||
*/
|
||||
data class IncomingCall(
|
||||
val operatorId: String,
|
||||
val operatorName: String,
|
||||
val callId: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* WebRTC Offer/Answer данные
|
||||
*/
|
||||
data class RTCSessionDescription(
|
||||
val type: String, // "offer" или "answer"
|
||||
val sdp: String
|
||||
)
|
||||
|
||||
/**
|
||||
* ICE Candidate данные
|
||||
*/
|
||||
data class RTCIceCandidate(
|
||||
val candidate: String,
|
||||
val sdpMid: String?,
|
||||
val sdpMLineIndex: Int?
|
||||
)
|
||||
@@ -1,16 +1,5 @@
|
||||
package com.example.godeye.models
|
||||
|
||||
/**
|
||||
* Информация об Android устройстве для регистрации на сервере
|
||||
* Соответствует требованиям ТЗ для Socket.IO регистрации
|
||||
*/
|
||||
data class DeviceInfo(
|
||||
val model: String,
|
||||
val androidVersion: String,
|
||||
val appVersion: String,
|
||||
val availableCameras: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Активная сессия камеры с оператором
|
||||
* Соответствует требованиям ТЗ для управления WebRTC сессиями
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.godeye.models
|
||||
|
||||
/**
|
||||
* Модель данных для статистики видео трансляции
|
||||
*/
|
||||
data class VideoStatistics(
|
||||
val fps: Int = 0,
|
||||
val width: Int = 0,
|
||||
val height: Int = 0,
|
||||
val bitrate: Long = 0,
|
||||
val bytesSent: Long = 0,
|
||||
val packetsLost: Int = 0,
|
||||
val jitter: Double = 0.0,
|
||||
val rtt: Long = 0,
|
||||
val framerate: Int = 0 // ДОБАВЛЕНО: для совместимости
|
||||
) {
|
||||
val resolution: String
|
||||
get() = "${width}x${height}"
|
||||
|
||||
val qualityLevel: VideoQuality
|
||||
get() = when {
|
||||
width >= 1920 -> VideoQuality.FULL_HD
|
||||
width >= 1280 -> VideoQuality.HD
|
||||
width >= 854 -> VideoQuality.SD
|
||||
else -> VideoQuality.LOW
|
||||
}
|
||||
}
|
||||
|
||||
enum class VideoQuality {
|
||||
LOW, SD, HD, FULL_HD
|
||||
}
|
||||
|
||||
/**
|
||||
* Состояние трансляции
|
||||
*/
|
||||
data class StreamingState(
|
||||
val isStreaming: Boolean = false,
|
||||
val sessionId: String? = null,
|
||||
val operatorId: String? = null,
|
||||
val startTime: Long = 0,
|
||||
val cameraType: String = "back",
|
||||
val statistics: VideoStatistics = VideoStatistics()
|
||||
) {
|
||||
val duration: Long
|
||||
get() = if (isStreaming) System.currentTimeMillis() - startTime else 0
|
||||
}
|
||||
310
app/src/main/java/com/example/godeye/network/SignalingClient.kt
Normal file
310
app/src/main/java/com/example/godeye/network/SignalingClient.kt
Normal file
@@ -0,0 +1,310 @@
|
||||
package com.example.godeye.network
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* WebSocket клиент для подключения к серверу сигналинга
|
||||
*/
|
||||
class SignalingClient {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SignalingClient"
|
||||
private const val DEFAULT_SERVER_URL = "ws://192.168.219.108:3001" // Замените на IP вашего компьютера
|
||||
private const val RECONNECT_DELAY = 5000L // 5 секунд
|
||||
}
|
||||
|
||||
private var webSocket: WebSocket? = null
|
||||
private var client: OkHttpClient? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// Состояние подключения
|
||||
private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
|
||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
// ID текущего клиента и сессии
|
||||
private var clientId: String? = null
|
||||
private var sessionId: String? = null
|
||||
|
||||
// Callback для обработки сообщений
|
||||
var onMessageReceived: ((JSONObject) -> Unit)? = null
|
||||
var onSessionCreated: ((String) -> Unit)? = null
|
||||
var onOperatorJoined: (() -> Unit)? = null
|
||||
|
||||
enum class ConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/**
|
||||
* Подключение к серверу сигналинга
|
||||
*/
|
||||
fun connect(serverUrl: String = DEFAULT_SERVER_URL) {
|
||||
if (_connectionState.value == ConnectionState.CONNECTING ||
|
||||
_connectionState.value == ConnectionState.CONNECTED) {
|
||||
Log.w(TAG, "Уже подключен или подключается")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "🔗 Подключение к серверу сигналинга: $serverUrl")
|
||||
_connectionState.value = ConnectionState.CONNECTING
|
||||
|
||||
try {
|
||||
client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.MILLISECONDS) // Бесконечное время ожидания для WebSocket
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(serverUrl)
|
||||
.build()
|
||||
|
||||
webSocket = client?.newWebSocket(request, object : WebSocketListener() {
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
Log.i(TAG, "✅ WebSocket подключен")
|
||||
_connectionState.value = ConnectionState.CONNECTED
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
Log.d(TAG, "📨 Получено сообщение: $text")
|
||||
handleMessage(text)
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
Log.i(TAG, "🔌 WebSocket закрыт: $code - $reason")
|
||||
_connectionState.value = ConnectionState.DISCONNECTED
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.e(TAG, "❌ Ошибка WebSocket: ${t.message}", t)
|
||||
_connectionState.value = ConnectionState.ERROR
|
||||
scheduleReconnect()
|
||||
}
|
||||
})
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Ошибка создания WebSocket: ${e.message}", e)
|
||||
_connectionState.value = ConnectionState.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение от сервера
|
||||
*/
|
||||
fun disconnect() {
|
||||
Log.i(TAG, "🔌 Отключение от сервера сигналинга")
|
||||
|
||||
scope.coroutineContext.cancelChildren()
|
||||
webSocket?.close(1000, "Пользователь отключился")
|
||||
webSocket = null
|
||||
client = null
|
||||
clientId = null
|
||||
sessionId = null
|
||||
_connectionState.value = ConnectionState.DISCONNECTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание новой сессии (устройство)
|
||||
*/
|
||||
fun createSession(deviceInfo: JSONObject) {
|
||||
if (_connectionState.value != ConnectionState.CONNECTED) {
|
||||
Log.w(TAG, "❌ Нет подключения к серверу")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "📱 Создание сессии...")
|
||||
|
||||
val message = JSONObject().apply {
|
||||
put("type", "create_session")
|
||||
put("device_info", deviceInfo)
|
||||
}
|
||||
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка SDP offer
|
||||
*/
|
||||
fun sendOffer(sdp: String) {
|
||||
if (sessionId == null) {
|
||||
Log.w(TAG, "❌ Нет активной сессии")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "📞 Отправка SDP Offer")
|
||||
|
||||
val message = JSONObject().apply {
|
||||
put("type", "offer")
|
||||
put("session_id", sessionId)
|
||||
put("sdp", JSONObject().apply {
|
||||
put("type", "offer")
|
||||
put("sdp", sdp)
|
||||
})
|
||||
}
|
||||
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка ICE кандидата
|
||||
*/
|
||||
fun sendIceCandidate(candidate: String, sdpMid: String, sdpMLineIndex: Int) {
|
||||
if (sessionId == null) {
|
||||
Log.w(TAG, "❌ Нет активной сессии")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "🧊 Отправка ICE candidate")
|
||||
|
||||
val message = JSONObject().apply {
|
||||
put("type", "ice_candidate")
|
||||
put("session_id", sessionId)
|
||||
put("candidate", JSONObject().apply {
|
||||
put("candidate", candidate)
|
||||
put("sdpMid", sdpMid)
|
||||
put("sdpMLineIndex", sdpMLineIndex)
|
||||
})
|
||||
}
|
||||
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение сессии
|
||||
*/
|
||||
fun hangup() {
|
||||
if (sessionId == null) {
|
||||
Log.w(TAG, "❌ Нет активной сессии")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "📴 Завершение сессии")
|
||||
|
||||
val message = JSONObject().apply {
|
||||
put("type", "hangup")
|
||||
put("session_id", sessionId)
|
||||
}
|
||||
|
||||
sendMessage(message)
|
||||
sessionId = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения через WebSocket
|
||||
*/
|
||||
private fun sendMessage(message: JSONObject) {
|
||||
if (_connectionState.value != ConnectionState.CONNECTED) {
|
||||
Log.w(TAG, "❌ Нет подключения для отправки сообщения")
|
||||
return
|
||||
}
|
||||
|
||||
val messageText = message.toString()
|
||||
Log.d(TAG, "📤 Отправка сообщения: $messageText")
|
||||
|
||||
webSocket?.send(messageText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка входящих сообщений
|
||||
*/
|
||||
private fun handleMessage(messageText: String) {
|
||||
try {
|
||||
val message = JSONObject(messageText)
|
||||
val type = message.optString("type")
|
||||
|
||||
Log.d(TAG, "🔍 Обработка сообщения типа: $type")
|
||||
|
||||
when (type) {
|
||||
"client_registered" -> {
|
||||
clientId = message.optString("client_id")
|
||||
Log.i(TAG, "🆔 Зарегистрирован как клиент: $clientId")
|
||||
}
|
||||
|
||||
"session_created" -> {
|
||||
sessionId = message.optString("session_id")
|
||||
Log.i(TAG, "✅ Сессия создана: $sessionId")
|
||||
onSessionCreated?.invoke(sessionId!!)
|
||||
}
|
||||
|
||||
"operator_joined" -> {
|
||||
Log.i(TAG, "👤 Оператор подключился")
|
||||
onOperatorJoined?.invoke()
|
||||
}
|
||||
|
||||
"answer" -> {
|
||||
Log.i(TAG, "📞 Получен SDP Answer")
|
||||
onMessageReceived?.invoke(message)
|
||||
}
|
||||
|
||||
"ice_candidate" -> {
|
||||
Log.d(TAG, "🧊 Получен ICE candidate")
|
||||
onMessageReceived?.invoke(message)
|
||||
}
|
||||
|
||||
"hangup" -> {
|
||||
Log.i(TAG, "📴 Сессия завершена оператором")
|
||||
sessionId = null
|
||||
onMessageReceived?.invoke(message)
|
||||
}
|
||||
|
||||
"error" -> {
|
||||
val errorMessage = message.optString("message", "Неизвестная ошибка")
|
||||
Log.e(TAG, "❌ Ошибка сервера: $errorMessage")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "⚠️ Неизвестный тип сообщения: $type")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Ошибка парсинга сообщения: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Планирование переподключения
|
||||
*/
|
||||
private fun scheduleReconnect() {
|
||||
if (_connectionState.value == ConnectionState.DISCONNECTED) {
|
||||
return // Не переподключаемся если отключились вручную
|
||||
}
|
||||
|
||||
Log.i(TAG, "🔄 Планирование переподключения через ${RECONNECT_DELAY}мс")
|
||||
|
||||
scope.launch {
|
||||
delay(RECONNECT_DELAY)
|
||||
if (_connectionState.value == ConnectionState.ERROR) {
|
||||
Log.i(TAG, "🔄 Попытка переподключения...")
|
||||
connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о текущей сессии
|
||||
*/
|
||||
fun getCurrentSessionId(): String? = sessionId
|
||||
|
||||
/**
|
||||
* Проверка активности сессии
|
||||
*/
|
||||
fun hasActiveSession(): Boolean = sessionId != null
|
||||
|
||||
/**
|
||||
* Освобождение ресурсов
|
||||
*/
|
||||
fun destroy() {
|
||||
disconnect()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.example.godeye.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.godeye.R
|
||||
import com.example.godeye.managers.ConnectionManager
|
||||
import com.example.godeye.managers.AutoApprovalManager
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Фоновый сервис для поддержания подключения к серверу
|
||||
*/
|
||||
class ConnectionService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ConnectionService"
|
||||
private const val NOTIFICATION_ID = 1000
|
||||
private const val CHANNEL_ID = "connection_service"
|
||||
|
||||
const val ACTION_START = "action_start"
|
||||
const val ACTION_STOP = "action_stop"
|
||||
const val ACTION_CONNECT = "action_connect"
|
||||
const val ACTION_DISCONNECT = "action_disconnect"
|
||||
}
|
||||
|
||||
private lateinit var connectionManager: ConnectionManager
|
||||
private lateinit var autoApprovalManager: AutoApprovalManager
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var statusUpdateJob: Job? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Service created")
|
||||
|
||||
preferenceManager = PreferenceManager(this)
|
||||
connectionManager = ConnectionManager(this, preferenceManager)
|
||||
autoApprovalManager = AutoApprovalManager(this, preferenceManager, connectionManager)
|
||||
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
createNotificationChannel()
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
Log.d(TAG, "Starting foreground service")
|
||||
startForeground(NOTIFICATION_ID, createNotification("Запуск сервиса..."))
|
||||
|
||||
// Автоподключение если включено
|
||||
if (preferenceManager.getAutoConnect()) {
|
||||
connectionManager.connect()
|
||||
}
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
Log.d(TAG, "Stopping service")
|
||||
stopSelf()
|
||||
}
|
||||
ACTION_CONNECT -> {
|
||||
Log.d(TAG, "Connect command received")
|
||||
connectionManager.connect()
|
||||
}
|
||||
ACTION_DISCONNECT -> {
|
||||
Log.d(TAG, "Disconnect command received")
|
||||
connectionManager.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY // Перезапускать сервис при остановке системой
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "Service destroyed")
|
||||
|
||||
statusUpdateJob?.cancel()
|
||||
serviceScope.cancel()
|
||||
connectionManager.cleanup()
|
||||
autoApprovalManager.cleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание канала уведомлений
|
||||
*/
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Подключение GodEye",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Статус подключения к серверу GodEye"
|
||||
setShowBadge(false)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание уведомления
|
||||
*/
|
||||
private fun createNotification(content: String): Notification {
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("GodEye Signal Center")
|
||||
.setContentText(content)
|
||||
.setSmallIcon(R.drawable.ic_camera_active)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Мониторинг состояния подключения
|
||||
*/
|
||||
private fun startMonitoring() {
|
||||
statusUpdateJob = serviceScope.launch {
|
||||
connectionManager.connectionState.collect { state ->
|
||||
val statusText = when (state) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> "Подключено к серверу"
|
||||
ConnectionManager.ConnectionState.CONNECTING -> "Подключение..."
|
||||
ConnectionManager.ConnectionState.ERROR -> "Ошибка подключения"
|
||||
ConnectionManager.ConnectionState.DISCONNECTED -> "Отключен"
|
||||
ConnectionManager.ConnectionState.RECONNECTING -> "Переподключение..."
|
||||
}
|
||||
|
||||
// Обновляем уведомление
|
||||
val notification = createNotification(statusText)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
|
||||
Log.d(TAG, "Connection state changed: $statusText")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.example.godeye.services
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.example.godeye.MainActivity
|
||||
import com.example.godeye.R
|
||||
import com.example.godeye.managers.SignalingManager
|
||||
import com.example.godeye.models.SessionState
|
||||
import com.example.godeye.models.SignalingState
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
|
||||
/**
|
||||
* Фоновый сервис для поддержания подключения к сигналлинг серверу
|
||||
*/
|
||||
class SignalingService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SignalingService"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val CHANNEL_ID = "signaling_channel"
|
||||
private const val CHANNEL_NAME = "Сигналлинг GodEye"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, SignalingService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val intent = Intent(context, SignalingService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var signalingManager: SignalingManager
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "SignalingService created")
|
||||
|
||||
signalingManager = SignalingManager(this)
|
||||
createNotificationChannel()
|
||||
|
||||
// Отслеживаем состояние подключения
|
||||
serviceScope.launch {
|
||||
signalingManager.signalingState.collect { state ->
|
||||
updateNotification(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Отслеживаем состояние сессии
|
||||
serviceScope.launch {
|
||||
signalingManager.sessionState.collect { sessionState ->
|
||||
handleSessionStateChange(sessionState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "SignalingService started")
|
||||
|
||||
// Запускаем в foreground режиме
|
||||
startForeground(NOTIFICATION_ID, createNotification(SignalingState.CONNECTING))
|
||||
|
||||
// Подключаемся к сигналлинг серверу
|
||||
signalingManager.start()
|
||||
|
||||
return START_STICKY // Перезапускать сервис при убийстве системой
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "SignalingService destroyed")
|
||||
|
||||
signalingManager.stop()
|
||||
serviceScope.cancel()
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null // Не поддерживаем binding
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание канала уведомлений
|
||||
*/
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Уведомления о состоянии подключения к серверу"
|
||||
setShowBadge(false)
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание уведомления
|
||||
*/
|
||||
private fun createNotification(state: SignalingState): Notification {
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val (title, content, icon) = when (state) {
|
||||
SignalingState.CONNECTED -> Triple(
|
||||
"GodEye подключен",
|
||||
"Готов к приему звонков от операторов",
|
||||
android.R.drawable.ic_dialog_info
|
||||
)
|
||||
SignalingState.CONNECTING -> Triple(
|
||||
"GodEye подключается",
|
||||
"Подключение к серверу...",
|
||||
android.R.drawable.ic_dialog_info
|
||||
)
|
||||
SignalingState.RECONNECTING -> Triple(
|
||||
"GodEye переподключается",
|
||||
"Восстановление соединения...",
|
||||
android.R.drawable.ic_dialog_alert
|
||||
)
|
||||
SignalingState.ERROR -> Triple(
|
||||
"GodEye - ошибка",
|
||||
"Проблема с подключением к серверу",
|
||||
android.R.drawable.ic_dialog_alert
|
||||
)
|
||||
SignalingState.DISCONNECTED -> Triple(
|
||||
"GodEye отключен",
|
||||
"Нет подключения к серверу",
|
||||
android.R.drawable.ic_dialog_alert
|
||||
)
|
||||
}
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setSmallIcon(icon)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление уведомления
|
||||
*/
|
||||
private fun updateNotification(state: SignalingState) {
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.notify(NOTIFICATION_ID, createNotification(state))
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка изменения состояния сессии
|
||||
*/
|
||||
private fun handleSessionStateChange(sessionState: SessionState) {
|
||||
when (sessionState) {
|
||||
SessionState.INCOMING -> {
|
||||
// Показываем уведомление о входящем звонке
|
||||
showIncomingCallNotification()
|
||||
}
|
||||
SessionState.ACTIVE -> {
|
||||
// Обновляем уведомление об активной сессии
|
||||
showActiveCallNotification()
|
||||
}
|
||||
SessionState.ENDED -> {
|
||||
// Возвращаемся к обычному уведомлению
|
||||
updateNotification(signalingManager.signalingState.value)
|
||||
}
|
||||
else -> {
|
||||
// Обычное состояние
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показ уведомления о входящем звонке
|
||||
*/
|
||||
private fun showIncomingCallNotification() {
|
||||
val currentCall = signalingManager.currentCall.value ?: return
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra("incoming_call", true)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Входящий звонок")
|
||||
.setContentText("От оператора: ${currentCall.operatorName}")
|
||||
.setSmallIcon(android.R.drawable.ic_menu_call)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setAutoCancel(true)
|
||||
.setFullScreenIntent(pendingIntent, true)
|
||||
.build()
|
||||
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.notify(NOTIFICATION_ID + 1, notification)
|
||||
}
|
||||
|
||||
/**
|
||||
* Показ уведомления об активной сессии
|
||||
*/
|
||||
private fun showActiveCallNotification() {
|
||||
val currentCall = signalingManager.currentCall.value ?: return
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Активная сессия")
|
||||
.setContentText("С оператором: ${currentCall.operatorName}")
|
||||
.setSmallIcon(android.R.drawable.ic_menu_camera)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
@@ -253,16 +253,17 @@ class SocketService : Service() {
|
||||
*/
|
||||
private fun registerDevice() {
|
||||
val deviceInfo = DeviceInfo(
|
||||
model = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}",
|
||||
deviceId = _deviceId.value,
|
||||
deviceName = "${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
|
||||
appVersion = "1.0.0"
|
||||
)
|
||||
|
||||
val registerData = JSONObject().apply {
|
||||
put("deviceId", _deviceId.value)
|
||||
put("deviceInfo", JSONObject().apply {
|
||||
put("model", deviceInfo.model)
|
||||
put("model", deviceInfo.deviceName)
|
||||
put("manufacturer", deviceInfo.manufacturer)
|
||||
put("androidVersion", deviceInfo.androidVersion)
|
||||
put("appVersion", deviceInfo.appVersion)
|
||||
put("availableCameras", JSONArray().apply {
|
||||
@@ -272,7 +273,7 @@ class SocketService : Service() {
|
||||
}
|
||||
|
||||
socket?.emit("register:android", registerData)
|
||||
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.model}")
|
||||
Logger.step("REGISTER_DEVICE", "Device registration sent: ${deviceInfo.deviceName}")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,6 +385,9 @@ class SocketService : Service() {
|
||||
* События WebRTC для обработки в UI
|
||||
*/
|
||||
sealed class WebRTCEvent {
|
||||
object Connected : WebRTCEvent()
|
||||
object Disconnected : WebRTCEvent()
|
||||
data class Error(val message: String) : WebRTCEvent()
|
||||
data class Offer(val sessionId: String, val offer: String) : WebRTCEvent()
|
||||
data class Answer(val sessionId: String, val answer: String) : WebRTCEvent()
|
||||
data class IceCandidate(
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
package com.example.godeye.signaling
|
||||
|
||||
import android.util.Log
|
||||
import com.example.godeye.models.*
|
||||
import com.google.gson.Gson
|
||||
import io.socket.client.IO
|
||||
import io.socket.client.Socket
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Сигналлинг клиент для WebRTC подключений
|
||||
*/
|
||||
class SignalingClient {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SignalingClient"
|
||||
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
|
||||
private const val DEFAULT_SERVER_URL = "ws://192.168.219.108:3000"
|
||||
}
|
||||
|
||||
private var socket: Socket? = null
|
||||
private val gson = Gson()
|
||||
|
||||
// Состояния
|
||||
private val _signalingState = MutableStateFlow(SignalingState.DISCONNECTED)
|
||||
val signalingState: StateFlow<SignalingState> = _signalingState.asStateFlow()
|
||||
|
||||
private val _sessionState = MutableStateFlow(SessionState.WAITING)
|
||||
val sessionState: StateFlow<SessionState> = _sessionState.asStateFlow()
|
||||
|
||||
private val _currentCall = MutableStateFlow<IncomingCall?>(null)
|
||||
val currentCall: StateFlow<IncomingCall?> = _currentCall.asStateFlow()
|
||||
|
||||
// Callbacks для WebRTC
|
||||
private var onOfferReceived: ((RTCSessionDescription) -> Unit)? = null
|
||||
private var onAnswerReceived: ((RTCSessionDescription) -> Unit)? = null
|
||||
private var onIceCandidateReceived: ((RTCIceCandidate) -> Unit)? = null
|
||||
private var onCallEnded: (() -> Unit)? = null
|
||||
|
||||
private var deviceId: String? = null
|
||||
|
||||
/**
|
||||
* Подключение к сигналлинг серверу
|
||||
*/
|
||||
fun connect(serverUrl: String = DEFAULT_SERVER_URL, deviceInfo: DeviceInfo) {
|
||||
Log.d(TAG, "Connecting to signaling server: $serverUrl")
|
||||
|
||||
try {
|
||||
_signalingState.value = SignalingState.CONNECTING
|
||||
this.deviceId = deviceInfo.model // Используем model как ID устройства
|
||||
|
||||
val options = IO.Options().apply {
|
||||
timeout = 5000
|
||||
reconnection = true
|
||||
reconnectionAttempts = 5
|
||||
reconnectionDelay = 1000
|
||||
}
|
||||
|
||||
socket = IO.socket(URI.create(serverUrl), options)
|
||||
|
||||
setupSocketListeners()
|
||||
socket?.connect()
|
||||
|
||||
// Регистрируем устройство после подключения
|
||||
socket?.on(Socket.EVENT_CONNECT) {
|
||||
Log.d(TAG, "Connected to signaling server")
|
||||
_signalingState.value = SignalingState.CONNECTED
|
||||
registerDevice(deviceInfo)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to connect to signaling server", e)
|
||||
_signalingState.value = SignalingState.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение от сигналлинг сервера
|
||||
*/
|
||||
fun disconnect() {
|
||||
Log.d(TAG, "Disconnecting from signaling server")
|
||||
|
||||
socket?.off()
|
||||
socket?.disconnect()
|
||||
socket = null
|
||||
|
||||
_signalingState.value = SignalingState.DISCONNECTED
|
||||
_sessionState.value = SessionState.WAITING
|
||||
_currentCall.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка слушателей Socket.IO
|
||||
*/
|
||||
private fun setupSocketListeners() {
|
||||
socket?.apply {
|
||||
|
||||
on(Socket.EVENT_DISCONNECT) {
|
||||
Log.d(TAG, "Disconnected from signaling server")
|
||||
_signalingState.value = SignalingState.DISCONNECTED
|
||||
}
|
||||
|
||||
on(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||
Log.e(TAG, "Connection error: ${args.contentToString()}")
|
||||
_signalingState.value = SignalingState.ERROR
|
||||
}
|
||||
|
||||
on("reconnect") {
|
||||
Log.d(TAG, "Reconnected to signaling server")
|
||||
_signalingState.value = SignalingState.CONNECTED
|
||||
}
|
||||
|
||||
on("reconnecting") {
|
||||
Log.d(TAG, "Reconnecting to signaling server")
|
||||
_signalingState.value = SignalingState.RECONNECTING
|
||||
}
|
||||
|
||||
// Обработка входящих вызовов
|
||||
on("operator_call") { args ->
|
||||
handleOperatorCall(args)
|
||||
}
|
||||
|
||||
// Обработка WebRTC сигналлинга
|
||||
on("offer") { args ->
|
||||
handleOffer(args)
|
||||
}
|
||||
|
||||
on("answer") { args ->
|
||||
handleAnswer(args)
|
||||
}
|
||||
|
||||
on("ice_candidate") { args ->
|
||||
handleIceCandidate(args)
|
||||
}
|
||||
|
||||
on("call_end") { args ->
|
||||
handleCallEnd(args)
|
||||
}
|
||||
|
||||
// Системные сообщения
|
||||
on("device_registered") { args ->
|
||||
Log.d(TAG, "Device registered successfully: ${args.contentToString()}")
|
||||
}
|
||||
|
||||
on("error") { args ->
|
||||
Log.e(TAG, "Server error: ${args.contentToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация устройства на сервере
|
||||
*/
|
||||
private fun registerDevice(deviceInfo: DeviceInfo) {
|
||||
Log.d(TAG, "Registering device: ${deviceInfo.model}")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.DEVICE_REGISTER,
|
||||
data = deviceInfo
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("device_register", json)
|
||||
|
||||
_sessionState.value = SessionState.WAITING
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка входящего вызова от оператора
|
||||
*/
|
||||
private fun handleOperatorCall(args: Array<Any>) {
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
Log.d(TAG, "Incoming operator call: $data")
|
||||
|
||||
val incomingCall = IncomingCall(
|
||||
operatorId = data.getString("operatorId"),
|
||||
operatorName = data.optString("operatorName", "Оператор"),
|
||||
callId = data.getString("callId"),
|
||||
timestamp = data.optLong("timestamp", System.currentTimeMillis())
|
||||
)
|
||||
|
||||
_currentCall.value = incomingCall
|
||||
_sessionState.value = SessionState.INCOMING
|
||||
|
||||
Log.d(TAG, "Incoming call from operator: ${incomingCall.operatorName}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling operator call", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Принятие вызова от оператора
|
||||
*/
|
||||
fun acceptCall() {
|
||||
val call = _currentCall.value ?: return
|
||||
|
||||
Log.d(TAG, "Accepting call from operator: ${call.operatorId}")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.CALL_ACCEPT,
|
||||
to = call.operatorId,
|
||||
data = mapOf("callId" to call.callId)
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("call_accept", json)
|
||||
|
||||
_sessionState.value = SessionState.ACTIVE
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение вызова от оператора
|
||||
*/
|
||||
fun rejectCall() {
|
||||
val call = _currentCall.value ?: return
|
||||
|
||||
Log.d(TAG, "Rejecting call from operator: ${call.operatorId}")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.CALL_REJECT,
|
||||
to = call.operatorId,
|
||||
data = mapOf("callId" to call.callId)
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("call_reject", json)
|
||||
|
||||
_sessionState.value = SessionState.WAITING
|
||||
_currentCall.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение вызова
|
||||
*/
|
||||
fun endCall() {
|
||||
val call = _currentCall.value ?: return
|
||||
|
||||
Log.d(TAG, "Ending call with operator: ${call.operatorId}")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.CALL_END,
|
||||
to = call.operatorId,
|
||||
data = mapOf("callId" to call.callId)
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("call_end", json)
|
||||
|
||||
_sessionState.value = SessionState.ENDED
|
||||
_currentCall.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка WebRTC Offer
|
||||
*/
|
||||
fun sendOffer(offer: RTCSessionDescription, operatorId: String) {
|
||||
Log.d(TAG, "Sending offer to operator: $operatorId")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.OFFER,
|
||||
to = operatorId,
|
||||
data = offer
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("offer", json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка WebRTC Answer
|
||||
*/
|
||||
fun sendAnswer(answer: RTCSessionDescription, operatorId: String) {
|
||||
Log.d(TAG, "Sending answer to operator: $operatorId")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.ANSWER,
|
||||
to = operatorId,
|
||||
data = answer
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("answer", json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка ICE Candidate
|
||||
*/
|
||||
fun sendIceCandidate(candidate: RTCIceCandidate, operatorId: String) {
|
||||
Log.d(TAG, "Sending ICE candidate to operator: $operatorId")
|
||||
|
||||
val message = SignalingMessage(
|
||||
type = SignalingMessageType.ICE_CANDIDATE,
|
||||
to = operatorId,
|
||||
data = candidate
|
||||
)
|
||||
|
||||
val json = JSONObject(gson.toJson(message))
|
||||
socket?.emit("ice_candidate", json)
|
||||
}
|
||||
|
||||
// Обработчики WebRTC сигналлинга
|
||||
private fun handleOffer(args: Array<Any>) {
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val offerData = data.getJSONObject("data")
|
||||
|
||||
val offer = RTCSessionDescription(
|
||||
type = offerData.getString("type"),
|
||||
sdp = offerData.getString("sdp")
|
||||
)
|
||||
|
||||
Log.d(TAG, "Received offer from operator")
|
||||
onOfferReceived?.invoke(offer)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling offer", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAnswer(args: Array<Any>) {
|
||||
try {
|
||||
val data = args[0] as JSONObject
|
||||
val answerData = data.getJSONObject("data")
|
||||
|
||||
val answer = RTCSessionDescription(
|
||||
type = answerData.getString("type"),
|
||||
sdp = answerData.getString("sdp")
|
||||
)
|
||||
|
||||
Log.d(TAG, "Received answer from operator")
|
||||
onAnswerReceived?.invoke(answer)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling answer", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Получение хоста сервера для ICE candidates
|
||||
*/
|
||||
fun getServerHost(): String? {
|
||||
return socket?.let { socket ->
|
||||
try {
|
||||
// ИСПРАВЛЕНИЕ: Используем правильный способ получения URI
|
||||
val socketUrl = socket.toString() // Получаем строковое представление
|
||||
val regex = """://([^:/]+)""".toRegex()
|
||||
val match = regex.find(socketUrl)
|
||||
match?.groupValues?.get(1) ?: "192.168.219.108" // Fallback на ваш сервер
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to extract server host", e)
|
||||
"192.168.219.108" // Fallback на ваш сервер
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ИСПРАВЛЕНИЕ: Улучшенная обработка ICE candidates с информацией о сервере
|
||||
*/
|
||||
private fun handleIceCandidate(args: Array<Any>) {
|
||||
try {
|
||||
if (args.isNotEmpty()) {
|
||||
val candidateData = args[0] as? JSONObject ?: return
|
||||
|
||||
val sessionId = candidateData.optString("sessionId", "")
|
||||
val candidateObj = candidateData.optJSONObject("candidate")
|
||||
|
||||
if (candidateObj != null) {
|
||||
// НОВОЕ: Добавляем информацию о сервере в ICE candidate
|
||||
val candidate = RTCIceCandidate(
|
||||
candidate = candidateObj.optString("candidate"),
|
||||
sdpMid = candidateObj.optString("sdpMid"),
|
||||
sdpMLineIndex = candidateObj.optInt("sdpMLineIndex")
|
||||
)
|
||||
|
||||
// Обогащаем candidate информацией о сигналинг сервере
|
||||
val enrichedCandidate = enrichIceCandidateWithServerInfo(candidate)
|
||||
|
||||
onIceCandidateReceived?.invoke(enrichedCandidate)
|
||||
|
||||
Log.d(TAG, "✅ ICE candidate received and enriched for session: $sessionId")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to handle ICE candidate", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Обогащение ICE candidate информацией о сервере
|
||||
*/
|
||||
private fun enrichIceCandidateWithServerInfo(candidate: RTCIceCandidate): RTCIceCandidate {
|
||||
val serverHost = getServerHost() ?: "192.168.219.108"
|
||||
|
||||
return RTCIceCandidate(
|
||||
candidate = candidate.candidate,
|
||||
sdpMid = candidate.sdpMid,
|
||||
sdpMLineIndex = candidate.sdpMLineIndex
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleCallEnd(args: Array<Any>) {
|
||||
try {
|
||||
Log.d(TAG, "Call ended by operator")
|
||||
|
||||
_sessionState.value = SessionState.ENDED
|
||||
_currentCall.value = null
|
||||
|
||||
onCallEnded?.invoke()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error handling call end", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Установка callback'ов для WebRTC
|
||||
fun setOnOfferReceived(callback: (RTCSessionDescription) -> Unit) {
|
||||
onOfferReceived = callback
|
||||
}
|
||||
|
||||
fun setOnAnswerReceived(callback: (RTCSessionDescription) -> Unit) {
|
||||
onAnswerReceived = callback
|
||||
}
|
||||
|
||||
fun setOnIceCandidateReceived(callback: (RTCIceCandidate) -> Unit) {
|
||||
onIceCandidateReceived = callback
|
||||
}
|
||||
|
||||
fun setOnCallEnded(callback: () -> Unit) {
|
||||
onCallEnded = callback
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
package com.example.godeye.streaming
|
||||
|
||||
import com.example.godeye.utils.Logger
|
||||
import java.io.*
|
||||
import java.net.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
/**
|
||||
* HLS Stream Manager - создает HTTP Live Streaming сервер на Android устройстве
|
||||
* Позволяет операторам подключаться через http://device_ip:8080/hls/stream.m3u8
|
||||
*/
|
||||
class HLSStreamManager {
|
||||
|
||||
private var httpServer: ServerSocket? = null
|
||||
private var isServerRunning = AtomicBoolean(false)
|
||||
private var serverThread: Thread? = null
|
||||
|
||||
private val serverPort = 8080
|
||||
private var deviceIP: String? = null
|
||||
private val segmentQueue = ConcurrentLinkedQueue<String>()
|
||||
private var segmentCounter = 0
|
||||
|
||||
init {
|
||||
detectDeviceIP()
|
||||
}
|
||||
|
||||
private fun detectDeviceIP() {
|
||||
try {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val networkInterface = interfaces.nextElement()
|
||||
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||
val addresses = networkInterface.inetAddresses
|
||||
while (addresses.hasMoreElements()) {
|
||||
val address = addresses.nextElement()
|
||||
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||
deviceIP = address.hostAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_IP_DETECTION", "Failed to detect IP", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun startStreaming(cameraType: String = "back"): String? {
|
||||
Logger.step("HLS_START_STREAMING", "🎬 Starting HLS server on port $serverPort")
|
||||
|
||||
try {
|
||||
if (isServerRunning.get()) {
|
||||
Logger.step("HLS_ALREADY_RUNNING", "HLS server already running")
|
||||
return "http://$deviceIP:$serverPort/hls/stream.m3u8"
|
||||
}
|
||||
|
||||
httpServer = ServerSocket(serverPort)
|
||||
isServerRunning.set(true)
|
||||
|
||||
serverThread = Thread {
|
||||
while (isServerRunning.get()) {
|
||||
try {
|
||||
val clientSocket = httpServer?.accept()
|
||||
if (clientSocket != null) {
|
||||
handleHTTPClient(clientSocket)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (isServerRunning.get()) {
|
||||
Logger.error("HLS_CLIENT_ERROR", "Error handling HTTP client", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serverThread?.start()
|
||||
|
||||
startSegmentGeneration()
|
||||
|
||||
val hlsUrl = "http://$deviceIP:$serverPort/hls/stream.m3u8"
|
||||
Logger.step("HLS_SERVER_STARTED", "✅ HLS server started: $hlsUrl")
|
||||
return hlsUrl
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_START_ERROR", "Failed to start HLS server", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHTTPClient(clientSocket: Socket) {
|
||||
Thread {
|
||||
try {
|
||||
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
|
||||
val output = PrintWriter(clientSocket.getOutputStream(), true)
|
||||
|
||||
val requestLine = input.readLine()
|
||||
Logger.step("HLS_REQUEST", "📡 HTTP request: $requestLine")
|
||||
|
||||
// Читаем остальные заголовки
|
||||
var line: String?
|
||||
while (input.readLine().also { line = it } != null && line!!.isNotEmpty()) {
|
||||
// Пропускаем заголовки
|
||||
}
|
||||
|
||||
when {
|
||||
requestLine.contains("GET /hls/stream.m3u8") -> {
|
||||
sendM3U8Playlist(output)
|
||||
}
|
||||
requestLine.contains("GET /hls/segment") -> {
|
||||
val segmentNumber = extractSegmentNumber(requestLine)
|
||||
sendSegment(output, segmentNumber)
|
||||
}
|
||||
requestLine.contains("GET /") -> {
|
||||
sendCORSHeaders(output)
|
||||
}
|
||||
else -> {
|
||||
send404(output)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_CLIENT_HANDLER", "Error handling HTTP client", e)
|
||||
} finally {
|
||||
clientSocket.close()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun sendM3U8Playlist(output: PrintWriter) {
|
||||
val playlist = generateM3U8Playlist()
|
||||
|
||||
output.println("HTTP/1.1 200 OK")
|
||||
output.println("Content-Type: application/vnd.apple.mpegurl")
|
||||
output.println("Content-Length: ${playlist.length}")
|
||||
output.println("Access-Control-Allow-Origin: *")
|
||||
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
|
||||
output.println("Access-Control-Allow-Headers: Content-Type")
|
||||
output.println("Cache-Control: no-cache")
|
||||
output.println()
|
||||
output.print(playlist)
|
||||
output.flush()
|
||||
|
||||
Logger.step("HLS_PLAYLIST_SENT", "📋 M3U8 playlist sent")
|
||||
}
|
||||
|
||||
private fun generateM3U8Playlist(): String {
|
||||
val playlist = StringBuilder()
|
||||
playlist.append("#EXTM3U\n")
|
||||
playlist.append("#EXT-X-VERSION:3\n")
|
||||
playlist.append("#EXT-X-TARGETDURATION:10\n")
|
||||
playlist.append("#EXT-X-MEDIA-SEQUENCE:$segmentCounter\n")
|
||||
playlist.append("#EXT-X-PLAYLIST-TYPE:EVENT\n")
|
||||
|
||||
// Добавляем последние сегменты
|
||||
val segments = segmentQueue.toList().takeLast(5)
|
||||
segments.forEach { segment ->
|
||||
playlist.append("#EXTINF:10.0,\n")
|
||||
playlist.append("$segment\n")
|
||||
}
|
||||
|
||||
return playlist.toString()
|
||||
}
|
||||
|
||||
private fun sendSegment(output: PrintWriter, segmentNumber: Int) {
|
||||
// В реальной реализации здесь будет отправка H.264/MPEG-TS сегмента
|
||||
val segmentData = generateDummySegment(segmentNumber)
|
||||
|
||||
output.println("HTTP/1.1 200 OK")
|
||||
output.println("Content-Type: video/mp2t")
|
||||
output.println("Content-Length: ${segmentData.size}")
|
||||
output.println("Access-Control-Allow-Origin: *")
|
||||
output.println()
|
||||
output.flush()
|
||||
|
||||
// Отправляем бинарные данные через OutputStream сокета
|
||||
val clientSocket = (output as? PrintWriter)?.let {
|
||||
// Получаем сокет из контекста (нужно передать его в метод)
|
||||
null // Временное решение
|
||||
}
|
||||
|
||||
Logger.step("HLS_SEGMENT_SENT", "🎥 Segment $segmentNumber sent")
|
||||
}
|
||||
|
||||
private fun generateDummySegment(segmentNumber: Int): ByteArray {
|
||||
// Заглушка - в реальной реализации здесь будут закодированные кадры
|
||||
return "DUMMY_TS_SEGMENT_$segmentNumber".toByteArray()
|
||||
}
|
||||
|
||||
private fun extractSegmentNumber(requestLine: String): Int {
|
||||
return try {
|
||||
val regex = "segment(\\d+)\\.ts".toRegex()
|
||||
val match = regex.find(requestLine)
|
||||
match?.groupValues?.get(1)?.toInt() ?: 0
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendCORSHeaders(output: PrintWriter) {
|
||||
output.println("HTTP/1.1 200 OK")
|
||||
output.println("Access-Control-Allow-Origin: *")
|
||||
output.println("Access-Control-Allow-Methods: GET, POST, OPTIONS")
|
||||
output.println("Access-Control-Allow-Headers: Content-Type")
|
||||
output.println("Content-Length: 0")
|
||||
output.println()
|
||||
output.flush()
|
||||
}
|
||||
|
||||
private fun send404(output: PrintWriter) {
|
||||
output.println("HTTP/1.1 404 Not Found")
|
||||
output.println("Content-Length: 0")
|
||||
output.println()
|
||||
output.flush()
|
||||
}
|
||||
|
||||
private fun startSegmentGeneration() {
|
||||
Thread {
|
||||
Logger.step("HLS_SEGMENT_GENERATION", "🎬 Starting HLS segment generation")
|
||||
|
||||
while (isServerRunning.get()) {
|
||||
try {
|
||||
// Генерируем новый сегмент каждые 10 секунд
|
||||
val segmentName = "segment${segmentCounter++}.ts"
|
||||
segmentQueue.offer(segmentName)
|
||||
|
||||
// Ограничиваем количество сегментов
|
||||
while (segmentQueue.size > 10) {
|
||||
segmentQueue.poll()
|
||||
}
|
||||
|
||||
Logger.step("HLS_SEGMENT_GENERATED", "📹 Generated segment: $segmentName")
|
||||
Thread.sleep(10000) // 10 секунд на сегмент
|
||||
|
||||
} catch (_: InterruptedException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_SEGMENT_ERROR", "Error generating segment", e)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("HLS_SWITCH_CAMERA", "🔄 Switching HLS camera to: $cameraType")
|
||||
// В реальной реализации здесь будет переключение источника кадров
|
||||
// TODO: Implement camera switching logic
|
||||
}
|
||||
|
||||
fun stopStreaming() {
|
||||
Logger.step("HLS_STOP_STREAMING", "🛑 Stopping HLS streaming")
|
||||
|
||||
try {
|
||||
isServerRunning.set(false)
|
||||
httpServer?.close()
|
||||
httpServer = null
|
||||
|
||||
serverThread?.interrupt()
|
||||
serverThread = null
|
||||
|
||||
segmentQueue.clear()
|
||||
segmentCounter = 0
|
||||
|
||||
Logger.step("HLS_STREAMING_STOPPED", "✅ HLS streaming stopped")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_STOP_ERROR", "Error stopping HLS streaming", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stopStreaming()
|
||||
}
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
package com.example.godeye.streaming
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.*
|
||||
import android.media.MediaRecorder
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import com.example.godeye.utils.Logger
|
||||
import java.io.*
|
||||
import java.net.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* RTSP Stream Manager - создает RTSP сервер на Android устройстве
|
||||
* Позволяет операторам подключаться напрямую через rtsp://device_ip:8554/live
|
||||
*/
|
||||
class RTSPStreamManager(private val context: Context) {
|
||||
|
||||
private var serverSocket: ServerSocket? = null
|
||||
private var isServerRunning = AtomicBoolean(false)
|
||||
private var serverThread: Thread? = null
|
||||
private val clientSockets = mutableListOf<Socket>()
|
||||
|
||||
private var cameraDevice: CameraDevice? = null
|
||||
private var captureSession: CameraCaptureSession? = null
|
||||
private var backgroundThread: HandlerThread? = null
|
||||
private var backgroundHandler: Handler? = null
|
||||
|
||||
private val serverPort = 8554
|
||||
private var deviceIP: String? = null
|
||||
|
||||
init {
|
||||
detectDeviceIP()
|
||||
startBackgroundThread()
|
||||
}
|
||||
|
||||
private fun detectDeviceIP() {
|
||||
try {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val networkInterface = interfaces.nextElement()
|
||||
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||
val addresses = networkInterface.inetAddresses
|
||||
while (addresses.hasMoreElements()) {
|
||||
val address = addresses.nextElement()
|
||||
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||
deviceIP = address.hostAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_IP_DETECTION", "Failed to detect IP", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startBackgroundThread() {
|
||||
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
|
||||
backgroundHandler = Handler(backgroundThread?.looper!!)
|
||||
}
|
||||
|
||||
fun startServer(cameraType: String = "back"): String? {
|
||||
Logger.step("RTSP_START_SERVER", "🎬 Starting RTSP server on port $serverPort")
|
||||
|
||||
try {
|
||||
if (isServerRunning.get()) {
|
||||
Logger.step("RTSP_ALREADY_RUNNING", "RTSP server already running")
|
||||
return "rtsp://$deviceIP:$serverPort/live"
|
||||
}
|
||||
|
||||
serverSocket = ServerSocket(serverPort)
|
||||
isServerRunning.set(true)
|
||||
|
||||
serverThread = Thread {
|
||||
while (isServerRunning.get()) {
|
||||
try {
|
||||
val clientSocket = serverSocket?.accept()
|
||||
if (clientSocket != null) {
|
||||
clientSockets.add(clientSocket)
|
||||
handleRTSPClient(clientSocket)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (isServerRunning.get()) {
|
||||
Logger.error("RTSP_CLIENT_ERROR", "Error handling client", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
serverThread?.start()
|
||||
|
||||
initializeCamera(cameraType)
|
||||
|
||||
val rtspUrl = "rtsp://$deviceIP:$serverPort/live"
|
||||
Logger.step("RTSP_SERVER_STARTED", "✅ RTSP server started: $rtspUrl")
|
||||
return rtspUrl
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_START_ERROR", "Failed to start RTSP server", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRTSPClient(clientSocket: Socket) {
|
||||
Thread {
|
||||
try {
|
||||
val input = BufferedReader(InputStreamReader(clientSocket.getInputStream()))
|
||||
val output = PrintWriter(clientSocket.getOutputStream(), true)
|
||||
|
||||
var line: String?
|
||||
val request = StringBuilder()
|
||||
|
||||
// Читаем RTSP запрос
|
||||
while (input.readLine().also { line = it } != null) {
|
||||
request.append(line).append("\n")
|
||||
if (line!!.isEmpty()) break
|
||||
}
|
||||
|
||||
val requestStr = request.toString()
|
||||
Logger.step("RTSP_REQUEST", "📡 RTSP request: ${requestStr.lines().firstOrNull()}")
|
||||
|
||||
when {
|
||||
requestStr.contains("OPTIONS") -> {
|
||||
sendRTSPResponse(output, "200 OK", mapOf(
|
||||
"Public" to "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE"
|
||||
))
|
||||
}
|
||||
requestStr.contains("DESCRIBE") -> {
|
||||
val sdp = generateSDP()
|
||||
sendRTSPResponse(output, "200 OK", mapOf(
|
||||
"Content-Type" to "application/sdp",
|
||||
"Content-Length" to sdp.length.toString()
|
||||
), sdp)
|
||||
}
|
||||
requestStr.contains("SETUP") -> {
|
||||
sendRTSPResponse(output, "200 OK", mapOf(
|
||||
"Transport" to "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001",
|
||||
"Session" to "12345678"
|
||||
))
|
||||
}
|
||||
requestStr.contains("PLAY") -> {
|
||||
sendRTSPResponse(output, "200 OK", mapOf(
|
||||
"Session" to "12345678"
|
||||
))
|
||||
startRTPStreaming(clientSocket)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_CLIENT_HANDLER", "Error handling RTSP client", e)
|
||||
} finally {
|
||||
clientSocket.close()
|
||||
clientSockets.remove(clientSocket)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun sendRTSPResponse(output: PrintWriter, status: String, headers: Map<String, String>, body: String = "") {
|
||||
output.println("RTSP/1.0 $status")
|
||||
output.println("CSeq: 1")
|
||||
headers.forEach { (key, value) ->
|
||||
output.println("$key: $value")
|
||||
}
|
||||
output.println()
|
||||
if (body.isNotEmpty()) {
|
||||
output.print(body)
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
|
||||
private fun generateSDP(): String {
|
||||
return """v=0
|
||||
o=- 0 0 IN IP4 $deviceIP
|
||||
s=Android Camera Stream
|
||||
c=IN IP4 $deviceIP
|
||||
t=0 0
|
||||
m=video 9000 RTP/AVP 96
|
||||
a=rtpmap:96 H264/90000
|
||||
a=fmtp:96 profile-level-id=42e01e
|
||||
a=control:streamid=0
|
||||
"""
|
||||
}
|
||||
|
||||
private fun startRTPStreaming(clientSocket: Socket) {
|
||||
Logger.step("RTSP_START_RTP", "🎥 Starting RTP streaming to client")
|
||||
|
||||
Thread {
|
||||
try {
|
||||
val rtpSocket = DatagramSocket(9000)
|
||||
val clientAddress = clientSocket.inetAddress
|
||||
|
||||
// Симуляция RTP пакетов (в реальной реализации здесь будут кадры с камеры)
|
||||
var sequenceNumber = 0
|
||||
val timestamp = System.currentTimeMillis()
|
||||
|
||||
while (isServerRunning.get() && !clientSocket.isClosed) {
|
||||
// Создаем простой RTP пакет
|
||||
val rtpPacket = createRTPPacket(sequenceNumber++, timestamp, "dummy_frame".toByteArray())
|
||||
val packet = DatagramPacket(rtpPacket, rtpPacket.size, clientAddress, 8000)
|
||||
rtpSocket.send(packet)
|
||||
|
||||
Thread.sleep(33) // ~30 FPS
|
||||
}
|
||||
|
||||
rtpSocket.close()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_RTP_ERROR", "Error in RTP streaming", e)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun createRTPPacket(sequenceNumber: Int, timestamp: Long, payload: ByteArray): ByteArray {
|
||||
val header = ByteArray(12)
|
||||
|
||||
// RTP Header
|
||||
header[0] = 0x80.toByte() // Version 2, no padding, no extension, no CC
|
||||
header[1] = 0x60.toByte() // Marker bit + Payload type (96)
|
||||
|
||||
// Sequence number
|
||||
header[2] = (sequenceNumber shr 8).toByte()
|
||||
header[3] = (sequenceNumber and 0xFF).toByte()
|
||||
|
||||
// Timestamp
|
||||
val ts = (timestamp and 0xFFFFFFFF).toInt()
|
||||
header[4] = (ts shr 24).toByte()
|
||||
header[5] = (ts shr 16).toByte()
|
||||
header[6] = (ts shr 8).toByte()
|
||||
header[7] = (ts and 0xFF).toByte()
|
||||
|
||||
// SSRC (synchronization source identifier)
|
||||
header[8] = 0x12.toByte()
|
||||
header[9] = 0x34.toByte()
|
||||
header[10] = 0x56.toByte()
|
||||
header[11] = 0x78.toByte()
|
||||
|
||||
return header + payload
|
||||
}
|
||||
|
||||
private fun initializeCamera(cameraType: String) {
|
||||
Logger.step("RTSP_INIT_CAMERA", "📷 Initializing camera for RTSP")
|
||||
|
||||
try {
|
||||
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
val cameraId = if (cameraType == "front") {
|
||||
cameraManager.cameraIdList.find {
|
||||
val characteristics = cameraManager.getCameraCharacteristics(it)
|
||||
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
||||
}
|
||||
} else {
|
||||
cameraManager.cameraIdList.find {
|
||||
val characteristics = cameraManager.getCameraCharacteristics(it)
|
||||
characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK
|
||||
}
|
||||
} ?: cameraManager.cameraIdList.firstOrNull()
|
||||
|
||||
if (cameraId != null) {
|
||||
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||
override fun onOpened(camera: CameraDevice) {
|
||||
cameraDevice = camera
|
||||
Logger.step("RTSP_CAMERA_OPENED", "✅ Camera opened for RTSP")
|
||||
createCaptureSession()
|
||||
}
|
||||
|
||||
override fun onDisconnected(camera: CameraDevice) {
|
||||
camera.close()
|
||||
cameraDevice = null
|
||||
}
|
||||
|
||||
override fun onError(camera: CameraDevice, error: Int) {
|
||||
Logger.error("RTSP_CAMERA_ERROR", "Camera error: $error")
|
||||
camera.close()
|
||||
cameraDevice = null
|
||||
}
|
||||
}, backgroundHandler)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_CAMERA_INIT_ERROR", "Failed to initialize camera", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCaptureSession() {
|
||||
// В реальной реализации здесь будет создание сессии захвата кадров
|
||||
// и их кодирование в H.264 для передачи через RTP
|
||||
Logger.step("RTSP_CAPTURE_SESSION", "📹 Camera capture session created")
|
||||
}
|
||||
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("RTSP_SWITCH_CAMERA", "🔄 Switching RTSP camera to: $cameraType")
|
||||
|
||||
// Закрываем текущую камеру и открываем новую
|
||||
cameraDevice?.close()
|
||||
initializeCamera(cameraType)
|
||||
}
|
||||
|
||||
fun stopServer() {
|
||||
Logger.step("RTSP_STOP_SERVER", "🛑 Stopping RTSP server")
|
||||
|
||||
try {
|
||||
isServerRunning.set(false)
|
||||
|
||||
clientSockets.forEach { it.close() }
|
||||
clientSockets.clear()
|
||||
|
||||
serverSocket?.close()
|
||||
serverSocket = null
|
||||
|
||||
cameraDevice?.close()
|
||||
cameraDevice = null
|
||||
|
||||
captureSession?.close()
|
||||
captureSession = null
|
||||
|
||||
serverThread?.interrupt()
|
||||
serverThread = null
|
||||
|
||||
Logger.step("RTSP_SERVER_STOPPED", "✅ RTSP server stopped")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_STOP_ERROR", "Error stopping RTSP server", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stopServer()
|
||||
backgroundThread?.quitSafely()
|
||||
backgroundThread = null
|
||||
backgroundHandler = null
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package com.example.godeye.streaming
|
||||
|
||||
import android.content.Context
|
||||
import com.example.godeye.utils.Logger
|
||||
import java.net.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* UDP Stream Manager - прямая передача видео через UDP для минимальной задержки
|
||||
* Позволяет операторам получать сырой видео поток через udp://device_ip:9999
|
||||
*/
|
||||
class UDPStreamManager(private val context: Context) {
|
||||
|
||||
private var udpSocket: DatagramSocket? = null
|
||||
private var isStreaming = AtomicBoolean(false)
|
||||
private var streamingThread: Thread? = null
|
||||
|
||||
private val streamingPort = 9999
|
||||
private var deviceIP: String? = null
|
||||
private var targetAddress: InetAddress? = null
|
||||
private var targetPort: Int = 0
|
||||
|
||||
init {
|
||||
detectDeviceIP()
|
||||
}
|
||||
|
||||
private fun detectDeviceIP() {
|
||||
try {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val networkInterface = interfaces.nextElement()
|
||||
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||
val addresses = networkInterface.inetAddresses
|
||||
while (addresses.hasMoreElements()) {
|
||||
val address = addresses.nextElement()
|
||||
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||
deviceIP = address.hostAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_IP_DETECTION", "Failed to detect IP", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun startStreaming(cameraType: String = "back"): String? {
|
||||
Logger.step("UDP_START_STREAMING", "🎬 Starting UDP streaming on port $streamingPort")
|
||||
|
||||
try {
|
||||
if (isStreaming.get()) {
|
||||
Logger.step("UDP_ALREADY_STREAMING", "UDP streaming already active")
|
||||
return "udp://$deviceIP:$streamingPort"
|
||||
}
|
||||
|
||||
udpSocket = DatagramSocket(streamingPort)
|
||||
isStreaming.set(true)
|
||||
|
||||
startFrameStreaming(cameraType)
|
||||
|
||||
val udpUrl = "udp://$deviceIP:$streamingPort"
|
||||
Logger.step("UDP_STREAMING_STARTED", "✅ UDP streaming started: $udpUrl")
|
||||
return udpUrl
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_START_ERROR", "Failed to start UDP streaming", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFrameStreaming(cameraType: String) {
|
||||
streamingThread = Thread {
|
||||
Logger.step("UDP_FRAME_STREAMING", "🎥 Starting UDP frame streaming")
|
||||
|
||||
var frameCounter = 0
|
||||
|
||||
while (isStreaming.get()) {
|
||||
try {
|
||||
// В реальной реализации здесь будут кадры с камеры
|
||||
val frameData = generateDummyFrame(frameCounter++, cameraType)
|
||||
|
||||
// Если есть подключенные клиенты, отправляем им кадры
|
||||
if (targetAddress != null && targetPort > 0) {
|
||||
sendFrame(frameData)
|
||||
}
|
||||
|
||||
// ~30 FPS
|
||||
Thread.sleep(33)
|
||||
|
||||
} catch (e: InterruptedException) {
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_FRAME_ERROR", "Error streaming frame", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
streamingThread?.start()
|
||||
}
|
||||
|
||||
private fun generateDummyFrame(frameNumber: Int, cameraType: String): ByteArray {
|
||||
// Заглушка - в реальной реализации здесь будут сжатые кадры H.264
|
||||
val frameHeader = ByteArray(8)
|
||||
|
||||
// Frame header: magic number + frame number + camera type
|
||||
frameHeader[0] = 0x47.toByte() // Magic 'G'
|
||||
frameHeader[1] = 0x45.toByte() // Magic 'E'
|
||||
frameHeader[2] = (frameNumber shr 24).toByte()
|
||||
frameHeader[3] = (frameNumber shr 16).toByte()
|
||||
frameHeader[4] = (frameNumber shr 8).toByte()
|
||||
frameHeader[5] = (frameNumber and 0xFF).toByte()
|
||||
frameHeader[6] = if (cameraType == "back") 0x00 else 0x01
|
||||
frameHeader[7] = 0x00 // Reserved
|
||||
|
||||
val frameData = "FRAME_${frameNumber}_${cameraType}_${System.currentTimeMillis()}".toByteArray()
|
||||
return frameHeader + frameData
|
||||
}
|
||||
|
||||
private fun sendFrame(frameData: ByteArray) {
|
||||
try {
|
||||
val packet = DatagramPacket(
|
||||
frameData,
|
||||
frameData.size,
|
||||
targetAddress,
|
||||
targetPort
|
||||
)
|
||||
udpSocket?.send(packet)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_SEND_FRAME", "Error sending UDP frame", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает адрес клиента для отправки кадров
|
||||
*/
|
||||
fun setClient(clientIP: String, clientPort: Int) {
|
||||
try {
|
||||
targetAddress = InetAddress.getByName(clientIP)
|
||||
targetPort = clientPort
|
||||
Logger.step("UDP_CLIENT_SET", "📡 UDP client set: $clientIP:$clientPort")
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_SET_CLIENT", "Error setting UDP client", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о UDP стриме для отправки клиенту
|
||||
*/
|
||||
fun getStreamInfo(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"protocol" to "udp",
|
||||
"ip" to (deviceIP ?: "unknown"),
|
||||
"port" to streamingPort,
|
||||
"url" to "udp://$deviceIP:$streamingPort",
|
||||
"format" to "raw_frames",
|
||||
"fps" to 30,
|
||||
"active" to isStreaming.get()
|
||||
)
|
||||
}
|
||||
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("UDP_SWITCH_CAMERA", "🔄 Switching UDP camera to: $cameraType")
|
||||
// В реальной реализации здесь будет переключение источника кадров
|
||||
// Новый тип камеры будет включен в следующие кадры
|
||||
}
|
||||
|
||||
fun stopStreaming() {
|
||||
Logger.step("UDP_STOP_STREAMING", "🛑 Stopping UDP streaming")
|
||||
|
||||
try {
|
||||
isStreaming.set(false)
|
||||
|
||||
streamingThread?.interrupt()
|
||||
streamingThread = null
|
||||
|
||||
udpSocket?.close()
|
||||
udpSocket = null
|
||||
|
||||
targetAddress = null
|
||||
targetPort = 0
|
||||
|
||||
Logger.step("UDP_STREAMING_STOPPED", "✅ UDP streaming stopped")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_STOP_ERROR", "Error stopping UDP streaming", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stopStreaming()
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
package com.example.godeye.streaming
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraManager
|
||||
import com.example.godeye.utils.Logger
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
import java.net.NetworkInterface
|
||||
import java.net.SocketException
|
||||
|
||||
/**
|
||||
* Unified Streaming Manager - управляет различными протоколами прямой передачи видео
|
||||
*
|
||||
* Поддерживаемые протоколы:
|
||||
* 1. WebRTC P2P - для веб-браузеров
|
||||
* 2. RTSP Server - для специализированных клиентов
|
||||
* 3. HTTP Live Streaming (HLS) - для универсальной совместимости
|
||||
* 4. Raw UDP Stream - для минимальной задержки
|
||||
*/
|
||||
class UnifiedStreamingManager(
|
||||
private val context: Context,
|
||||
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||
) {
|
||||
|
||||
// Состояния стриминга
|
||||
private val _streamingState = MutableStateFlow(StreamingState.STOPPED)
|
||||
val streamingState: StateFlow<StreamingState> = _streamingState.asStateFlow()
|
||||
|
||||
private val _availableProtocols = MutableStateFlow<List<StreamingProtocol>>(emptyList())
|
||||
val availableProtocols: StateFlow<List<StreamingProtocol>> = _availableProtocols.asStateFlow()
|
||||
|
||||
private val _activeStreams = MutableStateFlow<Map<String, StreamInfo>>(emptyMap())
|
||||
val activeStreams: StateFlow<Map<String, StreamInfo>> = _activeStreams.asStateFlow()
|
||||
|
||||
// Менеджеры протоколов
|
||||
private var webRTCManager: WebRTCStreamManager? = null
|
||||
private var rtspManager: RTSPStreamManager? = null
|
||||
private var hlsManager: HLSStreamManager? = null
|
||||
private var udpManager: UDPStreamManager? = null
|
||||
|
||||
private var deviceIP: String? = null
|
||||
|
||||
init {
|
||||
Logger.step("UNIFIED_STREAMING_INIT", "🎬 Initializing Unified Streaming Manager")
|
||||
detectDeviceIP()
|
||||
initializeProtocolSupport()
|
||||
}
|
||||
|
||||
/**
|
||||
* Определение IP адреса устройства для прямых соединений
|
||||
*/
|
||||
private fun detectDeviceIP() {
|
||||
try {
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val networkInterface = interfaces.nextElement()
|
||||
if (!networkInterface.isLoopback && networkInterface.isUp) {
|
||||
val addresses = networkInterface.inetAddresses
|
||||
while (addresses.hasMoreElements()) {
|
||||
val address = addresses.nextElement()
|
||||
if (!address.isLoopbackAddress && address.isSiteLocalAddress) {
|
||||
deviceIP = address.hostAddress
|
||||
Logger.step("DEVICE_IP_DETECTED", "📍 Device IP: $deviceIP")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SocketException) {
|
||||
Logger.error("IP_DETECTION_ERROR", "Failed to detect device IP", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация поддержки различных протоколов
|
||||
*/
|
||||
private fun initializeProtocolSupport() {
|
||||
val supportedProtocols = mutableListOf<StreamingProtocol>()
|
||||
|
||||
try {
|
||||
// WebRTC поддержка
|
||||
webRTCManager = WebRTCStreamManager(context, onSignalingMessage)
|
||||
supportedProtocols.add(
|
||||
StreamingProtocol(
|
||||
type = "webrtc",
|
||||
name = "WebRTC P2P",
|
||||
description = "Прямое P2P соединение для веб-браузеров",
|
||||
isSupported = true,
|
||||
connectionInfo = "Автоматическое P2P соединение"
|
||||
)
|
||||
)
|
||||
Logger.step("WEBRTC_SUPPORT", "✅ WebRTC protocol supported")
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_SUPPORT_ERROR", "WebRTC not supported", e)
|
||||
}
|
||||
|
||||
try {
|
||||
// RTSP Server поддержка
|
||||
rtspManager = RTSPStreamManager(context)
|
||||
supportedProtocols.add(
|
||||
StreamingProtocol(
|
||||
type = "rtsp",
|
||||
name = "RTSP Server",
|
||||
description = "RTSP сервер для специализированных клиентов",
|
||||
isSupported = true,
|
||||
connectionInfo = "rtsp://$deviceIP:8554/live"
|
||||
)
|
||||
)
|
||||
Logger.step("RTSP_SUPPORT", "✅ RTSP protocol supported")
|
||||
} catch (e: Exception) {
|
||||
Logger.error("RTSP_SUPPORT_ERROR", "RTSP not supported", e)
|
||||
}
|
||||
|
||||
try {
|
||||
// HLS поддержка
|
||||
hlsManager = HLSStreamManager()
|
||||
supportedProtocols.add(
|
||||
StreamingProtocol(
|
||||
type = "hls",
|
||||
name = "HTTP Live Streaming",
|
||||
description = "HLS стрим для универсальной совместимости",
|
||||
isSupported = true,
|
||||
connectionInfo = "http://$deviceIP:8080/hls/stream.m3u8"
|
||||
)
|
||||
)
|
||||
Logger.step("HLS_SUPPORT", "✅ HLS protocol supported")
|
||||
} catch (e: Exception) {
|
||||
Logger.error("HLS_SUPPORT_ERROR", "HLS not supported", e)
|
||||
}
|
||||
|
||||
try {
|
||||
// UDP Raw Stream поддержка
|
||||
udpManager = UDPStreamManager(context)
|
||||
supportedProtocols.add(
|
||||
StreamingProtocol(
|
||||
type = "udp",
|
||||
name = "Raw UDP Stream",
|
||||
description = "Прямой UDP поток для минимальной задержки",
|
||||
isSupported = true,
|
||||
connectionInfo = "udp://$deviceIP:9999"
|
||||
)
|
||||
)
|
||||
Logger.step("UDP_SUPPORT", "✅ UDP protocol supported")
|
||||
} catch (e: Exception) {
|
||||
Logger.error("UDP_SUPPORT_ERROR", "UDP not supported", e)
|
||||
}
|
||||
|
||||
_availableProtocols.value = supportedProtocols
|
||||
Logger.step("PROTOCOLS_INITIALIZED", "🎯 Initialized ${supportedProtocols.size} streaming protocols")
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск стриминга с выбранными протоколами
|
||||
*/
|
||||
fun startStreaming(
|
||||
sessionId: String,
|
||||
requestedProtocols: List<String> = listOf("webrtc", "rtsp"),
|
||||
cameraType: String = "back"
|
||||
) {
|
||||
Logger.step("START_STREAMING", "🎬 Starting streaming for session: $sessionId")
|
||||
Logger.step("STREAMING_PROTOCOLS", "📡 Requested protocols: ${requestedProtocols.joinToString(", ")}")
|
||||
|
||||
_streamingState.value = StreamingState.STARTING
|
||||
val activeStreams = mutableMapOf<String, StreamInfo>()
|
||||
|
||||
try {
|
||||
// Запуск WebRTC если запрошен
|
||||
if ("webrtc" in requestedProtocols && webRTCManager != null) {
|
||||
webRTCManager?.startStreaming(sessionId, cameraType)
|
||||
activeStreams["webrtc"] = StreamInfo(
|
||||
protocol = "webrtc",
|
||||
sessionId = sessionId,
|
||||
isActive = true,
|
||||
connectionUrl = "P2P Connection",
|
||||
startTime = System.currentTimeMillis()
|
||||
)
|
||||
Logger.step("WEBRTC_STARTED", "✅ WebRTC streaming started")
|
||||
}
|
||||
|
||||
// Запуск RTSP если запрошен
|
||||
if ("rtsp" in requestedProtocols && rtspManager != null) {
|
||||
val rtspUrl = rtspManager?.startServer(cameraType)
|
||||
if (rtspUrl != null) {
|
||||
activeStreams["rtsp"] = StreamInfo(
|
||||
protocol = "rtsp",
|
||||
sessionId = sessionId,
|
||||
isActive = true,
|
||||
connectionUrl = rtspUrl,
|
||||
startTime = System.currentTimeMillis()
|
||||
)
|
||||
Logger.step("RTSP_STARTED", "✅ RTSP streaming started: $rtspUrl")
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск HLS если запрошен
|
||||
if ("hls" in requestedProtocols && hlsManager != null) {
|
||||
val hlsUrl = hlsManager?.startStreaming(cameraType)
|
||||
if (hlsUrl != null) {
|
||||
activeStreams["hls"] = StreamInfo(
|
||||
protocol = "hls",
|
||||
sessionId = sessionId,
|
||||
isActive = true,
|
||||
connectionUrl = hlsUrl,
|
||||
startTime = System.currentTimeMillis()
|
||||
)
|
||||
Logger.step("HLS_STARTED", "✅ HLS streaming started: $hlsUrl")
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск UDP если запрошен
|
||||
if ("udp" in requestedProtocols && udpManager != null) {
|
||||
val udpUrl = udpManager?.startStreaming(cameraType)
|
||||
if (udpUrl != null) {
|
||||
activeStreams["udp"] = StreamInfo(
|
||||
protocol = "udp",
|
||||
sessionId = sessionId,
|
||||
isActive = true,
|
||||
connectionUrl = udpUrl,
|
||||
startTime = System.currentTimeMillis()
|
||||
)
|
||||
Logger.step("UDP_STARTED", "✅ UDP streaming started: $udpUrl")
|
||||
}
|
||||
}
|
||||
|
||||
_activeStreams.value = activeStreams
|
||||
_streamingState.value = if (activeStreams.isNotEmpty()) StreamingState.ACTIVE else StreamingState.ERROR
|
||||
|
||||
// Отправляем информацию о доступных стримах оператору через сигнальный сервер
|
||||
sendStreamingInfo(sessionId, activeStreams)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("START_STREAMING_ERROR", "Failed to start streaming", e)
|
||||
_streamingState.value = StreamingState.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка всех стримов
|
||||
*/
|
||||
fun stopStreaming(sessionId: String) {
|
||||
Logger.step("STOP_STREAMING", "🛑 Stopping streaming for session: $sessionId")
|
||||
|
||||
try {
|
||||
webRTCManager?.stopStreaming()
|
||||
rtspManager?.stopServer()
|
||||
hlsManager?.stopStreaming()
|
||||
udpManager?.stopStreaming()
|
||||
|
||||
_activeStreams.value = emptyMap()
|
||||
_streamingState.value = StreamingState.STOPPED
|
||||
|
||||
Logger.step("STREAMING_STOPPED", "✅ All streaming stopped")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("STOP_STREAMING_ERROR", "Error stopping streaming", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение камеры во всех активных стримах
|
||||
*/
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("SWITCH_CAMERA", "🔄 Switching camera to: $cameraType")
|
||||
|
||||
try {
|
||||
webRTCManager?.switchCamera(cameraType)
|
||||
rtspManager?.switchCamera(cameraType)
|
||||
hlsManager?.switchCamera(cameraType)
|
||||
udpManager?.switchCamera(cameraType)
|
||||
|
||||
Logger.step("CAMERA_SWITCHED", "✅ Camera switched to: $cameraType")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SWITCH_CAMERA_ERROR", "Error switching camera", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка информации о доступных стримах оператору
|
||||
*/
|
||||
private fun sendStreamingInfo(sessionId: String, streams: Map<String, StreamInfo>) {
|
||||
val streamingInfo = JSONObject().apply {
|
||||
put("type", "streaming_info")
|
||||
put("sessionId", sessionId)
|
||||
put("deviceIP", deviceIP)
|
||||
put("streams", JSONObject().apply {
|
||||
streams.forEach { (protocol, info) ->
|
||||
put(protocol, JSONObject().apply {
|
||||
put("url", info.connectionUrl)
|
||||
put("active", info.isActive)
|
||||
put("protocol", info.protocol)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onSignalingMessage(streamingInfo)
|
||||
Logger.step("STREAMING_INFO_SENT", "📡 Streaming info sent to operator")
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статистики стриминга
|
||||
*/
|
||||
fun getStreamingStats(): Map<String, Any> {
|
||||
val stats = mutableMapOf<String, Any>()
|
||||
|
||||
stats["state"] = _streamingState.value.name
|
||||
stats["activeStreams"] = _activeStreams.value.size
|
||||
stats["deviceIP"] = deviceIP ?: "unknown"
|
||||
stats["supportedProtocols"] = _availableProtocols.value.map { it.type }
|
||||
|
||||
_activeStreams.value.forEach { (protocol, info) ->
|
||||
stats["${protocol}_uptime"] = (System.currentTimeMillis() - info.startTime) / 1000
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
Logger.step("UNIFIED_STREAMING_DISPOSE", "🧹 Disposing Unified Streaming Manager")
|
||||
|
||||
try {
|
||||
stopStreaming("dispose")
|
||||
webRTCManager?.dispose()
|
||||
rtspManager?.dispose()
|
||||
hlsManager?.dispose()
|
||||
udpManager?.dispose()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("DISPOSE_ERROR", "Error during disposal", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Данные классы для стриминга
|
||||
enum class StreamingState {
|
||||
STOPPED, STARTING, ACTIVE, ERROR
|
||||
}
|
||||
|
||||
data class StreamingProtocol(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val isSupported: Boolean,
|
||||
val connectionInfo: String
|
||||
)
|
||||
|
||||
data class StreamInfo(
|
||||
val protocol: String,
|
||||
val sessionId: String,
|
||||
val isActive: Boolean,
|
||||
val connectionUrl: String,
|
||||
val startTime: Long
|
||||
)
|
||||
@@ -1,218 +0,0 @@
|
||||
package com.example.godeye.streaming
|
||||
|
||||
import android.content.Context
|
||||
import com.example.godeye.utils.Logger
|
||||
import org.json.JSONObject
|
||||
import org.webrtc.*
|
||||
import org.webrtc.audio.JavaAudioDeviceModule
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* WebRTC Stream Manager - улучшенная версия для прямого P2P соединения
|
||||
*/
|
||||
class WebRTCStreamManager(
|
||||
private val context: Context,
|
||||
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||
) {
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
private var peerConnection: PeerConnection? = null
|
||||
private var localVideoTrack: VideoTrack? = null
|
||||
private var localAudioTrack: AudioTrack? = null
|
||||
private var videoCapturer: CameraVideoCapturer? = null
|
||||
private var surfaceTextureHelper: SurfaceTextureHelper? = null
|
||||
|
||||
private val _connectionState = MutableStateFlow(PeerConnection.PeerConnectionState.NEW)
|
||||
val connectionState: StateFlow<PeerConnection.PeerConnectionState> = _connectionState.asStateFlow()
|
||||
|
||||
private val _isStreaming = MutableStateFlow(false)
|
||||
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
|
||||
|
||||
private val iceServers = listOf(
|
||||
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
|
||||
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
|
||||
)
|
||||
|
||||
init {
|
||||
initializePeerConnectionFactory()
|
||||
}
|
||||
|
||||
private fun initializePeerConnectionFactory() {
|
||||
val initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
|
||||
.setEnableInternalTracer(true)
|
||||
.createInitializationOptions()
|
||||
PeerConnectionFactory.initialize(initOptions)
|
||||
|
||||
val audioDeviceModule = JavaAudioDeviceModule.builder(context).createAudioDeviceModule()
|
||||
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setAudioDeviceModule(audioDeviceModule)
|
||||
.setVideoEncoderFactory(DefaultVideoEncoderFactory(
|
||||
EglBase.create().eglBaseContext, true, true))
|
||||
.setVideoDecoderFactory(DefaultVideoDecoderFactory(EglBase.create().eglBaseContext))
|
||||
.createPeerConnectionFactory()
|
||||
|
||||
Logger.step("WEBRTC_FACTORY_READY", "✅ WebRTC PeerConnectionFactory initialized")
|
||||
}
|
||||
|
||||
fun startStreaming(sessionId: String, cameraType: String = "back") {
|
||||
Logger.step("WEBRTC_START_STREAMING", "🎬 Starting WebRTC streaming for session: $sessionId")
|
||||
|
||||
try {
|
||||
createPeerConnection()
|
||||
initializeLocalMedia(cameraType)
|
||||
createOffer(sessionId)
|
||||
_isStreaming.value = true
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopStreaming() {
|
||||
Logger.step("WEBRTC_STOP_STREAMING", "🛑 Stopping WebRTC streaming")
|
||||
|
||||
try {
|
||||
videoCapturer?.stopCapture()
|
||||
videoCapturer?.dispose()
|
||||
|
||||
localVideoTrack?.dispose()
|
||||
localAudioTrack?.dispose()
|
||||
|
||||
peerConnection?.close()
|
||||
peerConnection = null
|
||||
|
||||
_isStreaming.value = false
|
||||
_connectionState.value = PeerConnection.PeerConnectionState.CLOSED
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_STOP_ERROR", "Error stopping WebRTC", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("WEBRTC_SWITCH_CAMERA", "🔄 Switching WebRTC camera to: $cameraType")
|
||||
(videoCapturer as? CameraVideoCapturer)?.switchCamera(null)
|
||||
}
|
||||
|
||||
private fun createPeerConnection() {
|
||||
val config = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
|
||||
}
|
||||
|
||||
peerConnection = peerConnectionFactory?.createPeerConnection(config, object : PeerConnection.Observer {
|
||||
override fun onSignalingChange(state: PeerConnection.SignalingState) {
|
||||
Logger.step("WEBRTC_SIGNALING", "Signaling state: $state")
|
||||
}
|
||||
|
||||
override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {
|
||||
Logger.step("WEBRTC_ICE_STATE", "ICE state: $state")
|
||||
}
|
||||
|
||||
override fun onConnectionChange(state: PeerConnection.PeerConnectionState) {
|
||||
_connectionState.value = state
|
||||
Logger.step("WEBRTC_CONNECTION_STATE", "Connection state: $state")
|
||||
}
|
||||
|
||||
override fun onIceCandidate(candidate: IceCandidate) {
|
||||
val candidateMsg = JSONObject().apply {
|
||||
put("type", "ice-candidate")
|
||||
put("candidate", candidate.sdp)
|
||||
put("sdpMLineIndex", candidate.sdpMLineIndex)
|
||||
put("sdpMid", candidate.sdpMid)
|
||||
}
|
||||
onSignalingMessage(candidateMsg)
|
||||
}
|
||||
|
||||
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {}
|
||||
override fun onAddStream(stream: MediaStream) {}
|
||||
override fun onRemoveStream(stream: MediaStream) {}
|
||||
override fun onDataChannel(channel: DataChannel) {}
|
||||
override fun onRenegotiationNeeded() {}
|
||||
override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {}
|
||||
override fun onIceConnectionReceivingChange(receiving: Boolean) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initializeLocalMedia(cameraType: String) {
|
||||
val videoSource = peerConnectionFactory?.createVideoSource(false)
|
||||
localVideoTrack = peerConnectionFactory?.createVideoTrack("video", videoSource)
|
||||
|
||||
val audioSource = peerConnectionFactory?.createAudioSource(MediaConstraints())
|
||||
localAudioTrack = peerConnectionFactory?.createAudioTrack("audio", audioSource)
|
||||
|
||||
val stream = peerConnectionFactory?.createLocalMediaStream("stream")
|
||||
localVideoTrack?.let { stream?.addTrack(it) }
|
||||
localAudioTrack?.let { stream?.addTrack(it) }
|
||||
|
||||
stream?.let { peerConnection?.addStream(it) }
|
||||
|
||||
initializeCamera(videoSource, cameraType)
|
||||
}
|
||||
|
||||
private fun initializeCamera(videoSource: VideoSource?, cameraType: String) {
|
||||
val cameraEnumerator = Camera2Enumerator(context)
|
||||
val cameraName = if (cameraType == "front") {
|
||||
cameraEnumerator.deviceNames.find { cameraEnumerator.isFrontFacing(it) }
|
||||
} else {
|
||||
cameraEnumerator.deviceNames.find { cameraEnumerator.isBackFacing(it) }
|
||||
} ?: cameraEnumerator.deviceNames.firstOrNull()
|
||||
|
||||
if (cameraName != null) {
|
||||
surfaceTextureHelper = SurfaceTextureHelper.create("CameraThread", EglBase.create().eglBaseContext)
|
||||
videoCapturer = cameraEnumerator.createCapturer(cameraName, null) as? CameraVideoCapturer
|
||||
|
||||
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource?.capturerObserver)
|
||||
videoCapturer?.startCapture(1280, 720, 30)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createOffer(sessionId: String) {
|
||||
val constraints = MediaConstraints()
|
||||
peerConnection?.createOffer(object : SdpObserver {
|
||||
override fun onCreateSuccess(desc: SessionDescription) {
|
||||
peerConnection?.setLocalDescription(object : SdpObserver {
|
||||
override fun onSetSuccess() {
|
||||
val offerMsg = JSONObject().apply {
|
||||
put("type", "offer")
|
||||
put("sessionId", sessionId)
|
||||
put("sdp", desc.description)
|
||||
}
|
||||
onSignalingMessage(offerMsg)
|
||||
}
|
||||
override fun onSetFailure(error: String) {}
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||
override fun onCreateFailure(p0: String?) {}
|
||||
}, desc)
|
||||
}
|
||||
override fun onCreateFailure(error: String) {}
|
||||
override fun onSetSuccess() {}
|
||||
override fun onSetFailure(error: String) {}
|
||||
}, constraints)
|
||||
}
|
||||
|
||||
fun handleAnswer(answerSdp: String) {
|
||||
val desc = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
|
||||
peerConnection?.setRemoteDescription(object : SdpObserver {
|
||||
override fun onSetSuccess() {
|
||||
Logger.step("WEBRTC_ANSWER_SET", "✅ WebRTC answer processed")
|
||||
}
|
||||
override fun onSetFailure(error: String) {}
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||
override fun onCreateFailure(p0: String?) {}
|
||||
}, desc)
|
||||
}
|
||||
|
||||
fun handleIceCandidate(candidate: String, sdpMLineIndex: Int, sdpMid: String) {
|
||||
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidate)
|
||||
peerConnection?.addIceCandidate(iceCandidate)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
stopStreaming()
|
||||
peerConnectionFactory?.dispose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package com.example.godeye.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.example.godeye.R
|
||||
import com.example.godeye.managers.ConnectionManager
|
||||
import com.example.godeye.utils.Logger
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import kotlinx.coroutines.launch
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* Активность для мониторинга видеопотока без предварительного просмотра
|
||||
* Показывает только статус трансляции и счетчик времени записи
|
||||
*/
|
||||
class StreamingMonitorActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var connectionManager: ConnectionManager
|
||||
private lateinit var preferenceManager: PreferenceManager
|
||||
|
||||
// UI элементы
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var recordingTimeText: TextView
|
||||
private lateinit var streamingStatusIndicator: View
|
||||
private lateinit var startButton: Button
|
||||
private lateinit var stopButton: Button
|
||||
private lateinit var switchCameraButton: Button
|
||||
private lateinit var disconnectButton: Button
|
||||
private lateinit var recordingIndicator: ImageView
|
||||
private lateinit var connectionStatusText: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_streaming_monitor)
|
||||
|
||||
initializeUI()
|
||||
initializeManagers()
|
||||
setupEventListeners()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация UI элементов
|
||||
*/
|
||||
private fun initializeUI() {
|
||||
statusText = findViewById(R.id.statusText)
|
||||
recordingTimeText = findViewById(R.id.recordingTimeText)
|
||||
streamingStatusIndicator = findViewById(R.id.streamingStatusIndicator)
|
||||
startButton = findViewById(R.id.startButton)
|
||||
stopButton = findViewById(R.id.stopButton)
|
||||
switchCameraButton = findViewById(R.id.switchCameraButton)
|
||||
disconnectButton = findViewById(R.id.disconnectButton)
|
||||
recordingIndicator = findViewById(R.id.recordingIndicator)
|
||||
connectionStatusText = findViewById(R.id.connectionStatusText)
|
||||
|
||||
// Начальное состояние
|
||||
updateUIForStatus(ConnectionManager.StreamingStatus.IDLE)
|
||||
recordingTimeText.text = "00:00:00"
|
||||
connectionStatusText.text = "Не подключено"
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация менеджеров
|
||||
*/
|
||||
private fun initializeManagers() {
|
||||
preferenceManager = PreferenceManager(this)
|
||||
connectionManager = ConnectionManager(this, preferenceManager)
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка слушателей событий
|
||||
*/
|
||||
private fun setupEventListeners() {
|
||||
startButton.setOnClickListener {
|
||||
startStreaming()
|
||||
}
|
||||
|
||||
stopButton.setOnClickListener {
|
||||
stopStreaming()
|
||||
}
|
||||
|
||||
switchCameraButton.setOnClickListener {
|
||||
switchCamera()
|
||||
}
|
||||
|
||||
disconnectButton.setOnClickListener {
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка наблюдателей за состоянием
|
||||
*/
|
||||
private fun setupObservers() {
|
||||
// Наблюдение за событиями соединения
|
||||
lifecycleScope.launch {
|
||||
connectionManager.events.collect { event ->
|
||||
when (event) {
|
||||
is ConnectionManager.ConnectionEvent.Connected -> {
|
||||
runOnUiThread {
|
||||
connectionStatusText.text = "Подключено к серверу"
|
||||
connectionStatusText.setTextColor(ContextCompat.getColor(this@StreamingMonitorActivity, R.color.success_green))
|
||||
Toast.makeText(this@StreamingMonitorActivity, "Подключено к серверу", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.Disconnected -> {
|
||||
runOnUiThread {
|
||||
connectionStatusText.text = "Отключено от сервера"
|
||||
connectionStatusText.setTextColor(ContextCompat.getColor(this@StreamingMonitorActivity, R.color.error_red))
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.Error -> {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@StreamingMonitorActivity, "Ошибка: ${event.message}", Toast.LENGTH_LONG).show()
|
||||
Logger.error("STREAMING_MONITOR_ERROR", event.message, null)
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.StatusUpdated -> {
|
||||
runOnUiThread {
|
||||
statusText.text = event.status
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.RecordingTimeUpdated -> {
|
||||
runOnUiThread {
|
||||
recordingTimeText.text = event.duration
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.StreamingStarted -> {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@StreamingMonitorActivity, "Трансляция началась", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
is ConnectionManager.ConnectionEvent.StreamingStopped -> {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@StreamingMonitorActivity, "Трансляция остановлена", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдение за статусом трансляции
|
||||
lifecycleScope.launch {
|
||||
connectionManager.streamingStatus.collect { status ->
|
||||
runOnUiThread {
|
||||
updateUIForStatus(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдение за длительностью записи
|
||||
lifecycleScope.launch {
|
||||
connectionManager.recordingDuration.collect { duration ->
|
||||
runOnUiThread {
|
||||
recordingTimeText.text = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Наблюдение за состоянием записи
|
||||
lifecycleScope.launch {
|
||||
connectionManager.isRecording.collect { isRecording ->
|
||||
runOnUiThread {
|
||||
updateRecordingIndicator(isRecording)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление UI в зависимости от статуса трансляции
|
||||
*/
|
||||
private fun updateUIForStatus(status: ConnectionManager.StreamingStatus) {
|
||||
when (status) {
|
||||
ConnectionManager.StreamingStatus.IDLE -> {
|
||||
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_idle))
|
||||
startButton.isEnabled = true
|
||||
stopButton.isEnabled = false
|
||||
switchCameraButton.isEnabled = false
|
||||
statusText.text = "Готов к трансляции"
|
||||
}
|
||||
ConnectionManager.StreamingStatus.CONNECTING -> {
|
||||
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_connecting))
|
||||
startButton.isEnabled = false
|
||||
stopButton.isEnabled = false
|
||||
switchCameraButton.isEnabled = false
|
||||
statusText.text = "Подключение..."
|
||||
}
|
||||
ConnectionManager.StreamingStatus.STREAMING -> {
|
||||
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_streaming))
|
||||
startButton.isEnabled = false
|
||||
stopButton.isEnabled = true
|
||||
switchCameraButton.isEnabled = true
|
||||
statusText.text = "Трансляция активна"
|
||||
}
|
||||
ConnectionManager.StreamingStatus.ERROR -> {
|
||||
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_error))
|
||||
startButton.isEnabled = true
|
||||
stopButton.isEnabled = false
|
||||
switchCameraButton.isEnabled = false
|
||||
statusText.text = "Ошибка трансляции"
|
||||
}
|
||||
ConnectionManager.StreamingStatus.STOPPING -> {
|
||||
streamingStatusIndicator.setBackgroundColor(ContextCompat.getColor(this, R.color.status_stopping))
|
||||
startButton.isEnabled = false
|
||||
stopButton.isEnabled = false
|
||||
switchCameraButton.isEnabled = false
|
||||
statusText.text = "Остановка трансляции..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление индикатора записи
|
||||
*/
|
||||
private fun updateRecordingIndicator(isRecording: Boolean) {
|
||||
if (isRecording) {
|
||||
recordingIndicator.setImageResource(R.drawable.ic_recording_active)
|
||||
recordingIndicator.visibility = View.VISIBLE
|
||||
// Добавляем анимацию мигания
|
||||
recordingIndicator.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(500)
|
||||
.withEndAction {
|
||||
recordingIndicator.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(500)
|
||||
.withEndAction {
|
||||
if (connectionManager.isCurrentlyRecording()) {
|
||||
updateRecordingIndicator(true) // Повторяем анимацию
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
recordingIndicator.visibility = View.GONE
|
||||
recordingIndicator.clearAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Начало трансляции
|
||||
*/
|
||||
private fun startStreaming() {
|
||||
try {
|
||||
Logger.step("MONITOR_START_STREAMING", "Starting streaming from monitor")
|
||||
|
||||
// Здесь можно получить sessionId и operatorId из Intent или диалога
|
||||
val sessionId = intent.getStringExtra("sessionId") ?: generateSessionId()
|
||||
val operatorId = intent.getStringExtra("operatorId") ?: "operator_1"
|
||||
val cameraType = intent.getStringExtra("cameraType") ?: "back"
|
||||
|
||||
connectionManager.startVideoStream(sessionId, operatorId, cameraType)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MONITOR_START_ERROR", "Failed to start streaming", e)
|
||||
Toast.makeText(this, "Ошибка запуска трансляции: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка трансляции
|
||||
*/
|
||||
private fun stopStreaming() {
|
||||
try {
|
||||
Logger.step("MONITOR_STOP_STREAMING", "Stopping streaming from monitor")
|
||||
connectionManager.stopAllStreaming()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MONITOR_STOP_ERROR", "Failed to stop streaming", e)
|
||||
Toast.makeText(this, "Ошибка остановки трансляции: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение камеры
|
||||
*/
|
||||
private fun switchCamera() {
|
||||
try {
|
||||
Logger.step("MONITOR_SWITCH_CAMERA", "Switching camera from monitor")
|
||||
connectionManager.switchCamera()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MONITOR_SWITCH_ERROR", "Failed to switch camera", e)
|
||||
Toast.makeText(this, "Ошибка переключения камеры: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение от сервера
|
||||
*/
|
||||
private fun disconnect() {
|
||||
try {
|
||||
Logger.step("MONITOR_DISCONNECT", "Disconnecting from monitor")
|
||||
connectionManager.disconnect()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MONITOR_DISCONNECT_ERROR", "Failed to disconnect", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация ID сессии
|
||||
*/
|
||||
private fun generateSessionId(): String {
|
||||
return "session_${System.currentTimeMillis()}"
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
try {
|
||||
connectionManager.dispose()
|
||||
} catch (e: Exception) {
|
||||
Logger.error("MONITOR_DISPOSE_ERROR", "Failed to dispose connection manager", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Не останавливаем трансляцию при уходе активности в фон
|
||||
Logger.step("MONITOR_PAUSE", "Monitor activity paused, streaming continues")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Logger.step("MONITOR_RESUME", "Monitor activity resumed")
|
||||
|
||||
// Обновляем UI с текущим состоянием
|
||||
runOnUiThread {
|
||||
updateUIForStatus(connectionManager.getCurrentStreamingStatus())
|
||||
recordingTimeText.text = connectionManager.getCurrentRecordingDuration()
|
||||
updateRecordingIndicator(connectionManager.isCurrentlyRecording())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.example.godeye.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.*
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.godeye.utils.Logger
|
||||
|
||||
/**
|
||||
* Компонент для отображения детальной информации о WebRTC соединениях
|
||||
*/
|
||||
@Composable
|
||||
fun NetworkTrafficMonitor(
|
||||
sessionId: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// Состояние для отслеживания логов
|
||||
var networkLogs by remember { mutableStateOf(listOf<NetworkLogEntry>()) }
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// Подписываемся на логи (в реальном приложении это бы было через Flow)
|
||||
LaunchedEffect(sessionId) {
|
||||
// Здесь мы бы подписались на логи, но для демонстрации создадим тестовые данные
|
||||
networkLogs = listOf(
|
||||
NetworkLogEntry("WEBRTC_ICE_SERVERS", "ICE servers: stun:stun.l.google.com:19302", System.currentTimeMillis()),
|
||||
NetworkLogEntry("WEBRTC_SDP_CANDIDATE", "UDP candidate: 192.168.1.100:54321", System.currentTimeMillis()),
|
||||
NetworkLogEntry("WEBRTC_CONNECTION_STATE", "ICE connection state: CONNECTED", System.currentTimeMillis()),
|
||||
NetworkLogEntry("WEBRTC_VIDEO_STATS", "Sending video to remote peer", System.currentTimeMillis())
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.9f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
// Заголовок с кнопкой раскрытия
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.NetworkCheck,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF00BFFF),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = "📡 Мониторинг трафика",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF00BFFF)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { isExpanded = !isExpanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = if (isExpanded) "Скрыть" else "Показать",
|
||||
tint = Color(0xFF00BFFF)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Информация о текущем соединении
|
||||
if (sessionId != null) {
|
||||
ConnectionSummary(sessionId = sessionId)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
// Логи сетевой активности
|
||||
Text(
|
||||
text = "Сетевые логи:",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 200.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(networkLogs.reversed()) { log ->
|
||||
NetworkLogItem(log = log)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Краткая информация когда свернуто
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (sessionId != null) {
|
||||
Text(
|
||||
text = "Активная сессия: $sessionId",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF40E0D0)
|
||||
)
|
||||
Text(
|
||||
text = "Статус: Передача видео активна",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "Нет активных соединений",
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectionSummary(sessionId: String) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF2D4A73).copy(alpha = 0.7f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Сессия:",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = sessionId,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Протокол:",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = "WebRTC (UDP/STUN)",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "Назначение:",
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = "Удаленный оператор",
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF40E0D0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkLogItem(log: NetworkLogEntry) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Color(0xFF0A1828).copy(alpha = 0.5f),
|
||||
RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Индикатор типа лога
|
||||
val (icon, color) = when {
|
||||
log.type.contains("CANDIDATE") -> Icons.Default.LocationOn to Color(0xFF4CAF50)
|
||||
log.type.contains("CONNECTION") -> Icons.Default.Link to Color(0xFF2196F3)
|
||||
log.type.contains("STATS") -> Icons.Default.Analytics to Color(0xFFFF9800)
|
||||
log.type.contains("SDP") -> Icons.Default.Code to Color(0xFF9C27B0)
|
||||
else -> Icons.Default.Info to Color(0xFF607D8B)
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = log.type,
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = log.message,
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFFE0F4FF),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
// Временная метка
|
||||
Text(
|
||||
text = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date(log.timestamp)),
|
||||
fontSize = 10.sp,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class NetworkLogEntry(
|
||||
val type: String,
|
||||
val message: String,
|
||||
val timestamp: Long
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
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.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.models.IncomingCall
|
||||
import com.example.godeye.models.SessionState
|
||||
import com.example.godeye.models.SignalingState
|
||||
|
||||
@Composable
|
||||
fun SignalingStatusCard(
|
||||
signalingState: SignalingState,
|
||||
sessionState: SessionState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = when (signalingState) {
|
||||
SignalingState.CONNECTED -> MaterialTheme.colorScheme.primaryContainer
|
||||
SignalingState.CONNECTING, SignalingState.RECONNECTING -> MaterialTheme.colorScheme.secondaryContainer
|
||||
SignalingState.ERROR -> MaterialTheme.colorScheme.errorContainer
|
||||
SignalingState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (signalingState) {
|
||||
SignalingState.CONNECTED -> Icons.Default.CheckCircle
|
||||
SignalingState.CONNECTING, SignalingState.RECONNECTING -> Icons.Default.Refresh
|
||||
SignalingState.ERROR -> Icons.Default.Warning
|
||||
SignalingState.DISCONNECTED -> Icons.Default.Close
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (signalingState) {
|
||||
SignalingState.CONNECTED -> MaterialTheme.colorScheme.primary
|
||||
SignalingState.CONNECTING, SignalingState.RECONNECTING -> MaterialTheme.colorScheme.secondary
|
||||
SignalingState.ERROR -> MaterialTheme.colorScheme.error
|
||||
SignalingState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = when (signalingState) {
|
||||
SignalingState.CONNECTED -> "Подключено к серверу"
|
||||
SignalingState.CONNECTING -> "Подключение..."
|
||||
SignalingState.RECONNECTING -> "Переподключение..."
|
||||
SignalingState.ERROR -> "Ошибка подключения"
|
||||
SignalingState.DISCONNECTED -> "Отключено"
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = when (sessionState) {
|
||||
SessionState.WAITING -> "Ожидание звонка от оператора"
|
||||
SessionState.INCOMING -> "Входящий звонок"
|
||||
SessionState.ACTIVE -> "Активная сессия"
|
||||
SessionState.ENDED -> "Сессия завершена"
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncomingCallDialog(
|
||||
incomingCall: IncomingCall,
|
||||
onAccept: () -> Unit,
|
||||
onReject: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { /* Не позволяем закрыть диалог без выбора */ }
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Иконка входящего звонка
|
||||
Icon(
|
||||
imageVector = Icons.Default.Phone,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Входящий звонок",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "От: ${incomingCall.operatorName}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "ID: ${incomingCall.operatorId}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Кнопка отклонения
|
||||
OutlinedButton(
|
||||
onClick = onReject,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Отклонить")
|
||||
}
|
||||
|
||||
// Кнопка принятия
|
||||
Button(
|
||||
onClick = onAccept,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Call,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Принять")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActiveCallCard(
|
||||
incomingCall: IncomingCall,
|
||||
onEndCall: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Активный звонок",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "С оператором: ${incomingCall.operatorName}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onEndCall,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Завершить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
package com.example.godeye.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.example.godeye.models.VideoStatistics
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StreamingVideoView(
|
||||
isStreaming: Boolean,
|
||||
statistics: VideoStatistics?,
|
||||
onStartStreaming: () -> Unit,
|
||||
onStopStreaming: () -> Unit,
|
||||
webRTCManager: com.example.godeye.managers.WebRTCManager? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
val context = LocalContext.current
|
||||
|
||||
// Состояния для сворачивания секций
|
||||
var cameraExpanded by remember { mutableStateOf(true) }
|
||||
var statsExpanded by remember { mutableStateOf(true) }
|
||||
var operatorExpanded by remember { mutableStateOf(true) }
|
||||
var trafficExpanded by remember { mutableStateOf(true) }
|
||||
var debugExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// Состояние для SurfaceViewRenderer
|
||||
var surfaceRenderer by remember { mutableStateOf<SurfaceViewRenderer?>(null) }
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Улучшенная инициализация камеры для предпросмотра
|
||||
LaunchedEffect(webRTCManager) {
|
||||
if (webRTCManager != null && surfaceRenderer == null) {
|
||||
try {
|
||||
android.util.Log.d("StreamingVideoView", "🎥 Initializing camera preview renderer...")
|
||||
|
||||
// Создаем SurfaceViewRenderer
|
||||
val renderer = SurfaceViewRenderer(context).apply {
|
||||
setScalingType(org.webrtc.RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
setEnableHardwareScaler(true)
|
||||
setMirror(false) // Отключаем зеркалирование для корректного отображения
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Инициализируем с EGL контекстом от WebRTCManager
|
||||
webRTCManager.getEglBaseContext()?.let { eglContext ->
|
||||
renderer.init(eglContext, null)
|
||||
surfaceRenderer = renderer
|
||||
|
||||
// КРИТИЧНО: Подключаем к WebRTCManager ПОСЛЕ инициализации
|
||||
webRTCManager.attachLocalVideoRenderer(renderer)
|
||||
|
||||
// НОВОЕ: Запускаем предпросмотр камеры немедленно
|
||||
webRTCManager.startCameraPreview()
|
||||
|
||||
android.util.Log.d("StreamingVideoView", "✅ Camera preview renderer initialized and started successfully")
|
||||
} ?: run {
|
||||
android.util.Log.e("StreamingVideoView", "❌ EGL context is null - cannot initialize camera preview")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("StreamingVideoView", "❌ Failed to initialize camera preview renderer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Правильная очистка рендерера при уничтожении
|
||||
DisposableEffect(surfaceRenderer, webRTCManager) {
|
||||
onDispose {
|
||||
try {
|
||||
if (surfaceRenderer != null && webRTCManager != null) {
|
||||
android.util.Log.d("StreamingVideoView", "🧹 Cleaning up camera preview renderer...")
|
||||
|
||||
// Останавливаем предпросмотр
|
||||
webRTCManager.stopCameraPreview()
|
||||
|
||||
// Отключаем рендерер
|
||||
webRTCManager.detachLocalVideoRenderer()
|
||||
|
||||
// Освобождаем ресурсы
|
||||
surfaceRenderer?.release()
|
||||
surfaceRenderer = null
|
||||
|
||||
android.util.Log.d("StreamingVideoView", "✅ Camera preview renderer cleaned up successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("StreamingVideoView", "❌ Failed to cleanup camera preview renderer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// 1. Плашка предпросмотра камеры
|
||||
CollapsibleCard(
|
||||
title = "📹 Предпросмотр камеры",
|
||||
expanded = cameraExpanded,
|
||||
onToggle = { cameraExpanded = !cameraExpanded }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(240.dp)
|
||||
.background(
|
||||
Color.Black,
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
// WebRTC SurfaceView для предпросмотра
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
SurfaceViewRenderer(context).apply {
|
||||
setScalingType(org.webrtc.RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||
setEnableHardwareScaler(true)
|
||||
// Инициализируем с EGL контекстом от WebRTCManager
|
||||
webRTCManager?.getEglBaseContext()?.let { eglContext ->
|
||||
try {
|
||||
init(eglContext, null)
|
||||
surfaceRenderer = this
|
||||
// ИСПРАВЛЕНИЕ: Подключаем рендерер к WebRTCManager сразу
|
||||
webRTCManager.attachLocalVideoRenderer(this)
|
||||
android.util.Log.d("StreamingVideoView", "✅ Camera renderer attached in factory")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("StreamingVideoView", "❌ Failed to init SurfaceViewRenderer", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
// Убираем дублирование инициализации
|
||||
android.util.Log.d("StreamingVideoView", "AndroidView update called")
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// Мигающий индикатор REC
|
||||
if (isStreaming) {
|
||||
RecordingIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Кнопки управления
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = if (isStreaming) onStopStreaming else onStartStreaming,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isStreaming) Color.Red else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
if (isStreaming) Icons.Default.Stop else Icons.Default.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(if (isStreaming) "Стоп" else "Старт")
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
webRTCManager?.switchCamera()
|
||||
},
|
||||
modifier = Modifier.background(
|
||||
Color.Black.copy(alpha = 0.5f),
|
||||
RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FlipCameraAndroid,
|
||||
contentDescription = "Переключить камеру",
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Характеристики видеопотока
|
||||
CollapsibleCard(
|
||||
title = "📊 Характеристики видеопотока",
|
||||
expanded = statsExpanded,
|
||||
onToggle = { statsExpanded = !statsExpanded }
|
||||
) {
|
||||
statistics?.let { stats ->
|
||||
VideoStatisticsContent(statistics = stats)
|
||||
} ?: run {
|
||||
Text(
|
||||
"Статистика недоступна",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Подключенный оператор
|
||||
CollapsibleCard(
|
||||
title = "👤 Подключенный оператор",
|
||||
expanded = operatorExpanded,
|
||||
onToggle = { operatorExpanded = !operatorExpanded }
|
||||
) {
|
||||
OperatorInfoContent(isConnected = isStreaming)
|
||||
}
|
||||
|
||||
// 4. Переданный трафик
|
||||
CollapsibleCard(
|
||||
title = "📡 Переданный трафик",
|
||||
expanded = trafficExpanded,
|
||||
onToggle = { trafficExpanded = !trafficExpanded }
|
||||
) {
|
||||
TrafficInfoContent(statistics = statistics)
|
||||
}
|
||||
|
||||
// 5. Отладка и диагностика
|
||||
CollapsibleCard(
|
||||
title = "🔧 Отладка и диагностика",
|
||||
expanded = debugExpanded,
|
||||
onToggle = { debugExpanded = !debugExpanded }
|
||||
) {
|
||||
DebugInfoContent()
|
||||
}
|
||||
|
||||
// Мониторинг сетевого трафика (всегда внизу)
|
||||
NetworkTrafficMonitor(
|
||||
sessionId = if (isStreaming) "active_session" else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordingIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
kotlinx.coroutines.delay(1000)
|
||||
isVisible = !isVisible
|
||||
}
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Red),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FiberManualRecord,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(8.dp)
|
||||
)
|
||||
Text(
|
||||
"REC",
|
||||
color = Color.White,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CollapsibleCard(
|
||||
title: String,
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column {
|
||||
// Заголовок (всегда видим)
|
||||
Surface(
|
||||
onClick = onToggle,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = if (expanded) "Свернуть" else "Развернуть",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Содержимое (показывается только при развернутом состоянии)
|
||||
if (expanded) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VideoStatisticsContent(statistics: VideoStatistics) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
StatisticRow("FPS", "${statistics.fps}")
|
||||
StatisticRow("Разрешение", "${statistics.width}x${statistics.height}")
|
||||
StatisticRow("Битрейт", formatBitrate(statistics.bitrate))
|
||||
StatisticRow("Переданные кадры", "${statistics.framerate}")
|
||||
StatisticRow("Потерянные пакеты", "${statistics.packetsLost}")
|
||||
StatisticRow("Задержка (RTT)", "${statistics.rtt}ms")
|
||||
StatisticRow("Джиттер", "${String.format("%.2f", statistics.jitter)}ms")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OperatorInfoContent(isConnected: Boolean) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Circle,
|
||||
contentDescription = null,
|
||||
tint = if (isConnected) Color.Green else Color.Gray,
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
Text(
|
||||
if (isConnected) "Подключен" else "Не подключен",
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (isConnected) Color.Green else Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
StatisticRow("ID оператора", "OP_12345")
|
||||
StatisticRow("Время сессии", "05:23")
|
||||
StatisticRow("Качество связи", "Отлично")
|
||||
StatisticRow("Тип камеры", "Основная")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrafficInfoContent(statistics: VideoStatistics?) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
statistics?.let { stats ->
|
||||
StatisticRow("Всего отправлено", formatBytes(stats.bytesSent))
|
||||
StatisticRow("Скорость", formatBitrate(stats.bitrate))
|
||||
StatisticRow("Сжатие", "H.264")
|
||||
StatisticRow("Использовано трафика", formatBytes(stats.bytesSent))
|
||||
} ?: run {
|
||||
Text(
|
||||
"Нет данных о трафике",
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugInfoContent() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
// Информация о соединении
|
||||
Text(
|
||||
"Информация о соединении",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
StatisticRow("WebRTC состояние", "CONNECTED")
|
||||
StatisticRow("ICE состояние", "COMPLETED")
|
||||
StatisticRow("Signaling", "STABLE")
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Сетевая информация
|
||||
Text(
|
||||
"Сетевая информация",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
StatisticRow("Локальный IP", "192.168.1.105")
|
||||
StatisticRow("Внешний IP", "203.45.67.89")
|
||||
StatisticRow("STUN сервер", "stun.l.google.com:19302")
|
||||
StatisticRow("Порт", "51234")
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Кнопки действий
|
||||
Text(
|
||||
"Действия отладки",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { /* Тест соединения */ },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Тест связи", fontSize = 12.sp)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { /* Перезапуск */ },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Перезапуск", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { /* Логи */ },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Экспорт логов", fontSize = 12.sp)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { /* Диагностика */ },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("Диагностика", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Переключатели отладки
|
||||
HorizontalDivider()
|
||||
|
||||
Text(
|
||||
"Настройки отладки",
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
var verboseLogging by remember { mutableStateOf(false) }
|
||||
var showStatistics by remember { mutableStateOf(true) }
|
||||
var autoReconnect by remember { mutableStateOf(true) }
|
||||
|
||||
DebugSwitchRow(
|
||||
"Детальное логирование",
|
||||
verboseLogging
|
||||
) { verboseLogging = it }
|
||||
|
||||
DebugSwitchRow(
|
||||
"Показывать статистику",
|
||||
showStatistics
|
||||
) { showStatistics = it }
|
||||
|
||||
DebugSwitchRow(
|
||||
"Автоперезподключение",
|
||||
autoReconnect
|
||||
) { autoReconnect = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DebugSwitchRow(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = Modifier.height(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatisticRow(
|
||||
label: String,
|
||||
value: String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long): String {
|
||||
return when {
|
||||
bytes < 1024 -> "$bytes B"
|
||||
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
|
||||
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
|
||||
else -> "${bytes / (1024 * 1024 * 1024)} GB"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatBitrate(bitrate: Long): String {
|
||||
return when {
|
||||
bitrate < 1000 -> "${bitrate} bps"
|
||||
bitrate < 1000000 -> "${bitrate / 1000} Kbps"
|
||||
else -> "${bitrate / 1000000} Mbps"
|
||||
}
|
||||
}
|
||||
850
app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt
Normal file
850
app/src/main/java/com/example/godeye/ui/screens/MainScreen.kt
Normal file
@@ -0,0 +1,850 @@
|
||||
package com.example.godeye.ui.screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.example.godeye.managers.AutoApprovalManager
|
||||
import com.example.godeye.managers.ConnectionManager
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import com.example.godeye.utils.PermissionHelper
|
||||
import com.example.godeye.ui.components.StreamingVideoView
|
||||
import com.example.godeye.models.StreamingState
|
||||
import com.example.godeye.managers.WebRTCManager
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
connectionManager: ConnectionManager,
|
||||
autoApprovalManager: AutoApprovalManager,
|
||||
preferenceManager: PreferenceManager,
|
||||
permissionHelper: PermissionHelper,
|
||||
onOpenSettings: () -> Unit
|
||||
) {
|
||||
val connectionState by connectionManager.connectionState.collectAsState(initial = ConnectionManager.ConnectionState.DISCONNECTED)
|
||||
val deviceInfo by connectionManager.deviceInfo.collectAsState(initial = null)
|
||||
val activeSessions by autoApprovalManager.activeSessions.collectAsState(initial = emptyList())
|
||||
val pendingRequest by autoApprovalManager.pendingRequest.collectAsState(initial = null)
|
||||
|
||||
val serverUrl by preferenceManager.serverUrl.collectAsState(initial = "")
|
||||
val deviceName by preferenceManager.deviceName.collectAsState(initial = "")
|
||||
val autoConnect by preferenceManager.autoConnect.collectAsState(initial = false)
|
||||
val autoApprove by preferenceManager.autoApprove.collectAsState(initial = false)
|
||||
|
||||
// НОВОЕ: Состояние трансляции
|
||||
val streamingState by connectionManager.webRTCManager.streamingState.collectAsState()
|
||||
val isStreamingMode = streamingState.isStreaming
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF0A1828),
|
||||
Color(0xFF1E3A5F),
|
||||
Color(0xFF2D4A73)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (isStreamingMode) {
|
||||
// НОВОЕ: Режим трансляции с видео и статистикой
|
||||
StreamingModeContent(
|
||||
streamingState = streamingState,
|
||||
onSwitchCamera = { connectionManager.webRTCManager.switchCamera() },
|
||||
onStopStreaming = {
|
||||
streamingState.sessionId?.let { sessionId ->
|
||||
connectionManager.webRTCManager.stopStreaming(sessionId)
|
||||
}
|
||||
},
|
||||
webRTCManager = connectionManager.webRTCManager // Передаем WebRTCManager
|
||||
)
|
||||
} else {
|
||||
// Обычный режим с плашками настроек
|
||||
NormalModeContent(
|
||||
connectionState = connectionState,
|
||||
deviceInfo = deviceInfo,
|
||||
activeSessions = activeSessions,
|
||||
pendingRequest = pendingRequest,
|
||||
serverUrl = serverUrl,
|
||||
deviceName = deviceName,
|
||||
autoConnect = autoConnect,
|
||||
autoApprove = autoApprove,
|
||||
connectionManager = connectionManager,
|
||||
autoApprovalManager = autoApprovalManager,
|
||||
preferenceManager = preferenceManager,
|
||||
permissionHelper = permissionHelper,
|
||||
onOpenSettings = onOpenSettings
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StreamingModeContent(
|
||||
streamingState: StreamingState,
|
||||
onSwitchCamera: () -> Unit,
|
||||
onStopStreaming: () -> Unit,
|
||||
webRTCManager: com.example.godeye.managers.WebRTCManager // Добавлен параметр webRTCManager
|
||||
) {
|
||||
StreamingVideoView(
|
||||
isStreaming = streamingState.isStreaming,
|
||||
statistics = streamingState.statistics,
|
||||
onStartStreaming = { /* Здесь можно добавить логику запуска трансляции */ },
|
||||
onStopStreaming = onStopStreaming,
|
||||
webRTCManager = webRTCManager,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NormalModeContent(
|
||||
connectionState: ConnectionManager.ConnectionState,
|
||||
deviceInfo: Any?,
|
||||
activeSessions: List<Any>,
|
||||
pendingRequest: Any?,
|
||||
serverUrl: String,
|
||||
deviceName: String,
|
||||
autoConnect: Boolean,
|
||||
autoApprove: Boolean,
|
||||
connectionManager: ConnectionManager,
|
||||
autoApprovalManager: AutoApprovalManager,
|
||||
preferenceManager: PreferenceManager,
|
||||
permissionHelper: PermissionHelper,
|
||||
onOpenSettings: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок с настройками
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "GodEye",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF00BFFF)
|
||||
)
|
||||
Text(
|
||||
text = "Signal Center",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color(0xFF40E0D0)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onOpenSettings,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
Color(0xFF1A2B3D),
|
||||
CircleShape
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Settings,
|
||||
contentDescription = "Настройки",
|
||||
tint = Color(0xFF00BFFF)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Карточка статуса подключения
|
||||
ConnectionStatusCard(
|
||||
connectionState = connectionState,
|
||||
serverUrl = serverUrl,
|
||||
deviceName = deviceName,
|
||||
deviceInfo = deviceInfo as? com.example.godeye.models.DeviceInfo,
|
||||
onConnect = { connectionManager.connect() },
|
||||
onDisconnect = { connectionManager.disconnect() }
|
||||
)
|
||||
|
||||
// Карточка настроек
|
||||
SettingsOverviewCard(
|
||||
autoConnect = autoConnect,
|
||||
autoApprove = autoApprove,
|
||||
hasAllPermissions = permissionHelper.hasAllPermissions(),
|
||||
onOpenSettings = onOpenSettings
|
||||
)
|
||||
|
||||
// Тестовая кнопка для проверки автоподтверждения
|
||||
if (connectionState == ConnectionManager.ConnectionState.CONNECTED) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF2D4A22).copy(alpha = 0.8f)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFF4CAF50))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Тестирование автоподтверждения",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF4CAF50)
|
||||
)
|
||||
Text(
|
||||
text = "Имитирует запрос камеры от оператора",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFFB3B3B3)
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { connectionManager.testAutoApproval() },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF4CAF50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = "Тест"
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Тест")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pending запрос (если есть)
|
||||
pendingRequest?.let { request ->
|
||||
if (request is org.json.JSONObject) {
|
||||
PendingRequestCard(
|
||||
request = request,
|
||||
onApprove = { sessionId, operatorId, cameraType ->
|
||||
autoApprovalManager.approveRequest(sessionId, operatorId, cameraType)
|
||||
},
|
||||
onDeny = { sessionId ->
|
||||
autoApprovalManager.denyRequest(sessionId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Активные сессии
|
||||
if (activeSessions.isNotEmpty()) {
|
||||
Text(
|
||||
text = "Активные сессии (${activeSessions.size})",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF00BFFF)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(activeSessions) { session ->
|
||||
if (session is AutoApprovalManager.CameraSession) {
|
||||
ActiveSessionCard(session = session)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Пустое состояние
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.8f)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFF40E0D0).copy(alpha = 0.3f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = Color(0xFF40E0D0)
|
||||
)
|
||||
Text(
|
||||
text = "Нет активных сессий",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
Text(
|
||||
text = "Ожидание запросов от операторов",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectionStatusCard(
|
||||
connectionState: ConnectionManager.ConnectionState,
|
||||
serverUrl: String,
|
||||
deviceName: String,
|
||||
deviceInfo: com.example.godeye.models.DeviceInfo?,
|
||||
onConnect: () -> Unit,
|
||||
onDisconnect: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = when (connectionState) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primaryContainer
|
||||
ConnectionManager.ConnectionState.CONNECTING -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
ConnectionManager.ConnectionState.ERROR -> MaterialTheme.colorScheme.errorContainer
|
||||
ConnectionManager.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceVariant
|
||||
ConnectionManager.ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.tertiaryContainer
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Подключение к серверу",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
when (connectionState) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> {
|
||||
Button(
|
||||
onClick = onDisconnect,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text("Отключиться")
|
||||
}
|
||||
}
|
||||
ConnectionManager.ConnectionState.CONNECTING -> {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
else -> {
|
||||
Button(onClick = onConnect) {
|
||||
Text("Подключиться")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Статус
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
when (connectionState) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> Icons.Default.CheckCircle
|
||||
ConnectionManager.ConnectionState.CONNECTING -> Icons.Default.Refresh
|
||||
ConnectionManager.ConnectionState.ERROR -> Icons.Default.Warning
|
||||
ConnectionManager.ConnectionState.DISCONNECTED -> Icons.Default.Close
|
||||
ConnectionManager.ConnectionState.RECONNECTING -> Icons.Default.Refresh
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (connectionState) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||
ConnectionManager.ConnectionState.CONNECTING -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||
ConnectionManager.ConnectionState.ERROR -> MaterialTheme.colorScheme.onErrorContainer
|
||||
ConnectionManager.ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
ConnectionManager.ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = when (connectionState) {
|
||||
ConnectionManager.ConnectionState.CONNECTED -> "Подключено"
|
||||
ConnectionManager.ConnectionState.CONNECTING -> "Подключение..."
|
||||
ConnectionManager.ConnectionState.ERROR -> "Ошибка соединения"
|
||||
ConnectionManager.ConnectionState.DISCONNECTED -> "Отключено"
|
||||
ConnectionManager.ConnectionState.RECONNECTING -> "Переподключение..."
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
// Информация о сервере
|
||||
Text(
|
||||
text = "Сервер: $serverUrl",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Устройство: $deviceName",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
if (deviceInfo is com.example.godeye.models.DeviceInfo) {
|
||||
Text(
|
||||
text = "Модель: ${deviceInfo.manufacturer} ${deviceInfo.model} (Android ${deviceInfo.androidVersion})",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsOverviewCard(
|
||||
autoConnect: Boolean,
|
||||
autoApprove: Boolean,
|
||||
hasAllPermissions: Boolean,
|
||||
onOpenSettings: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Быстрые настройки",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
TextButton(onClick = onOpenSettings) {
|
||||
Text("Все настройки")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
// Статусы настроек
|
||||
SettingStatusRow("Автоподключение", autoConnect)
|
||||
SettingStatusRow("Автоподтверждение", autoApprove, isWarning = autoApprove)
|
||||
SettingStatusRow("Разрешения", hasAllPermissions, isError = !hasAllPermissions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingStatusRow(
|
||||
title: String,
|
||||
enabled: Boolean,
|
||||
isWarning: Boolean = false,
|
||||
isError: Boolean = false
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (enabled) Icons.Default.Check else Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = when {
|
||||
isError -> MaterialTheme.colorScheme.error
|
||||
isWarning -> MaterialTheme.colorScheme.tertiary
|
||||
enabled -> MaterialTheme.colorScheme.primary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = when {
|
||||
isError -> MaterialTheme.colorScheme.error
|
||||
isWarning -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PendingRequestCard(
|
||||
request: org.json.JSONObject,
|
||||
onApprove: (String, String, String) -> Unit,
|
||||
onDeny: (String) -> Unit
|
||||
) {
|
||||
val sessionId = request.optString("sessionId")
|
||||
val operatorId = request.optString("operatorId")
|
||||
val cameraType = request.optString("cameraType", "back")
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF2D4A22).copy(alpha = 0.8f)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFFFF9800))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.NotificationImportant,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFFF9800),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Входящий запрос камеры",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFFFF9800)
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = Color(0xFFFF9800).copy(alpha = 0.3f),
|
||||
thickness = 1.dp
|
||||
)
|
||||
|
||||
// UUID оператора
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "UUID оператора:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = operatorId,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE0F4FF),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
Color(0xFF2D4A73).copy(alpha = 0.5f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Информация о запросе
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Запрашиваемая камера:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = getCameraDisplayName(cameraType),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
text = "ID сессии:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = sessionId.take(8) + "...",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Предупреждение
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFFFF9800).copy(alpha = 0.1f)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFFFF9800).copy(alpha = 0.3f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFFFF9800),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Убедитесь, что вы доверяете этому оператору",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFFF9800)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопки действий
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = { onDeny(sessionId) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = Color(0xFFF44336)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFFF44336))
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Отклонить")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { onApprove(sessionId, operatorId, cameraType) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF4CAF50)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Разрешить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveSessionCard(
|
||||
session: AutoApprovalManager.CameraSession
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(0xFF1A2B3D).copy(alpha = 0.9f)
|
||||
),
|
||||
border = BorderStroke(1.dp, Color(0xFF00BFFF).copy(alpha = 0.5f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Заголовок с состоянием
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Подключенный оператор",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF00BFFF)
|
||||
)
|
||||
|
||||
// Индикатор состояния
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.background(
|
||||
color = when (session.streamingState) {
|
||||
AutoApprovalManager.StreamingState.STREAMING -> Color(0xFF4CAF50)
|
||||
AutoApprovalManager.StreamingState.CONNECTING -> Color(0xFFFF9800)
|
||||
AutoApprovalManager.StreamingState.ERROR -> Color(0xFFF44336)
|
||||
else -> Color(0xFF9E9E9E)
|
||||
},
|
||||
shape = CircleShape
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = when (session.streamingState) {
|
||||
AutoApprovalManager.StreamingState.STREAMING -> "Активна"
|
||||
AutoApprovalManager.StreamingState.CONNECTING -> "Подключение"
|
||||
AutoApprovalManager.StreamingState.ERROR -> "Ошибка"
|
||||
else -> "Ожидание"
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
color = Color(0xFF40E0D0).copy(alpha = 0.3f),
|
||||
thickness = 1.dp
|
||||
)
|
||||
|
||||
// UUID оператора
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "UUID оператора:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = session.operatorId,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFFE0F4FF),
|
||||
// Делаем текст выделяемым для копирования
|
||||
modifier = Modifier
|
||||
.background(
|
||||
Color(0xFF2D4A73).copy(alpha = 0.5f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Информация о камере и времени
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Камера:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = getCameraDisplayName(session.cameraType),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
text = "Длительность:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFB0C4DE)
|
||||
)
|
||||
Text(
|
||||
text = formatDuration(System.currentTimeMillis() - session.startTime),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFFE0F4FF)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Дополнительная информация
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (session.isAutoApproved) "Автоматически одобрено" else "Одобрено вручную",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (session.isAutoApproved) Color(0xFF4CAF50) else Color(0xFF2196F3)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Default.Videocam,
|
||||
contentDescription = "Камера активна",
|
||||
tint = Color(0xFF00BFFF),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для форматирования времени
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = (durationMs / 1000) % 60
|
||||
val minutes = (durationMs / (1000 * 60)) % 60
|
||||
val hours = (durationMs / (1000 * 60 * 60)) % 24
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
else -> String.format("%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для отображения типа камеры
|
||||
private fun getCameraDisplayName(cameraType: String): String {
|
||||
return when (cameraType) {
|
||||
"back" -> "Основная камера"
|
||||
"front" -> "Фронтальная камера"
|
||||
"wide" -> "Широкоугольная камера"
|
||||
"telephoto" -> "Телеобъектив"
|
||||
else -> "Камера"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package com.example.godeye.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.example.godeye.utils.PreferenceManager
|
||||
import com.example.godeye.utils.PermissionHelper
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
preferenceManager: PreferenceManager,
|
||||
permissionHelper: PermissionHelper,
|
||||
onNavigateBack: () -> Unit
|
||||
) {
|
||||
val serverUrl by preferenceManager.serverUrl.collectAsStateWithLifecycle()
|
||||
val deviceName by preferenceManager.deviceName.collectAsStateWithLifecycle()
|
||||
val autoConnect by preferenceManager.autoConnect.collectAsStateWithLifecycle()
|
||||
val autoApprove by preferenceManager.autoApprove.collectAsStateWithLifecycle()
|
||||
|
||||
var tempServerUrl by remember(serverUrl) { mutableStateOf(serverUrl) }
|
||||
var tempDeviceName by remember(deviceName) { mutableStateOf(deviceName) }
|
||||
var showPermissionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Заголовок
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Настройки",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.Close, contentDescription = "Закрыть")
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Секция "Подключение к серверу"
|
||||
SettingsSection(title = "Подключение к серверу") {
|
||||
// URL сервера
|
||||
OutlinedTextField(
|
||||
value = tempServerUrl,
|
||||
onValueChange = { tempServerUrl = it },
|
||||
label = { Text("URL сервера") },
|
||||
placeholder = { Text("http://192.168.219.108:3001") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Кнопка сохранения URL
|
||||
Button(
|
||||
onClick = {
|
||||
if (tempServerUrl.isNotBlank()) {
|
||||
preferenceManager.setServerUrl(tempServerUrl.trim())
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = tempServerUrl.trim() != serverUrl
|
||||
) {
|
||||
Icon(Icons.Default.Done, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Сохранить URL")
|
||||
}
|
||||
|
||||
// Автоматическое подключение
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Автоматическое подключение",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
text = "Подключаться к серверу при запуске приложения",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = autoConnect,
|
||||
onCheckedChange = { preferenceManager.setAutoConnect(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Секция "Устройство"
|
||||
SettingsSection(title = "Устройство") {
|
||||
// Имя устройства
|
||||
OutlinedTextField(
|
||||
value = tempDeviceName,
|
||||
onValueChange = { tempDeviceName = it },
|
||||
label = { Text("Имя устройства") },
|
||||
placeholder = { Text("Android Device") },
|
||||
leadingIcon = { Icon(Icons.Default.Phone, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Кнопка сохранения имени
|
||||
Button(
|
||||
onClick = {
|
||||
if (tempDeviceName.isNotBlank()) {
|
||||
preferenceManager.setDeviceName(tempDeviceName.trim())
|
||||
}
|
||||
},
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = tempDeviceName.trim() != deviceName
|
||||
) {
|
||||
Icon(Icons.Default.Done, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Сохранить имя")
|
||||
}
|
||||
|
||||
// ID устройства (только для чтения)
|
||||
OutlinedTextField(
|
||||
value = preferenceManager.getDeviceId(),
|
||||
onValueChange = { },
|
||||
label = { Text("ID устройства") },
|
||||
leadingIcon = { Icon(Icons.Default.Info, contentDescription = null) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = false,
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
// Секция "Безопасность"
|
||||
SettingsSection(title = "Безопасность") {
|
||||
// Автоматическое подтверждение
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (autoApprove)
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Автоматическое подтверждение",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (autoApprove)
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = if (autoApprove) "⚠️ Операторы получат доступ без подтверждения"
|
||||
else "Запрашивать подтверждение для каждого запроса",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (autoApprove)
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = autoApprove,
|
||||
onCheckedChange = { preferenceManager.setAutoApprove(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Секция "Разрешения"
|
||||
SettingsSection(title = "Разрешения") {
|
||||
val hasAllPermissions = permissionHelper.hasAllPermissions()
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (hasAllPermissions)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (hasAllPermissions) Icons.Default.CheckCircle else Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = if (hasAllPermissions)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
text = if (hasAllPermissions) "Все разрешения предоставлены" else "Требуются разрешения",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (hasAllPermissions)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
|
||||
// Список разрешений
|
||||
PermissionHelper.REQUIRED_PERMISSIONS.forEach { permission ->
|
||||
val hasPermission = permissionHelper.hasPermission(permission)
|
||||
val permissionName = getPermissionDisplayName(permission)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (hasPermission) Icons.Default.Check else Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = if (hasPermission)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
Text(
|
||||
text = permissionName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (hasAllPermissions)
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAllPermissions) {
|
||||
Button(
|
||||
onClick = { showPermissionDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(Icons.Default.Lock, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Запросить разрешения")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Диалог запроса разрешений
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPermissionDialog = false },
|
||||
title = { Text("Разрешения приложения") },
|
||||
text = {
|
||||
Text("Для корректной работы приложению необходимы разрешения на доступ к камере, микрофону и интернету.")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showPermissionDialog = false
|
||||
permissionHelper.requestPermissions { granted ->
|
||||
// Результат обработается автоматически через recomposition
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Предоставить")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showPermissionDialog = false }) {
|
||||
Text("Отмена")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSection(
|
||||
title: String,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPermissionDisplayName(permission: String): String {
|
||||
return when (permission) {
|
||||
android.Manifest.permission.CAMERA -> "Камера"
|
||||
android.Manifest.permission.RECORD_AUDIO -> "Микрофон"
|
||||
android.Manifest.permission.INTERNET -> "Интернет"
|
||||
android.Manifest.permission.ACCESS_NETWORK_STATE -> "Состояние сети"
|
||||
android.Manifest.permission.WAKE_LOCK -> "Предотвращение блокировки"
|
||||
android.Manifest.permission.POST_NOTIFICATIONS -> "Уведомления"
|
||||
else -> permission.split(".").lastOrNull() ?: permission
|
||||
}
|
||||
}
|
||||
@@ -5,93 +5,54 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Цветовая палитра GodEye согласно ТЗ
|
||||
*/
|
||||
// Цветовая схема GodEye
|
||||
object GodEyeColors {
|
||||
// Основные цвета приложения
|
||||
val BlackPure = Color(0xFF000000)
|
||||
val BlackSoft = Color(0xFF1A1A1A)
|
||||
val BlackMedium = Color(0xFF2D2D2D)
|
||||
val Primary = Color(0xFF1976D2)
|
||||
val PrimaryVariant = Color(0xFF1565C0)
|
||||
val Secondary = Color(0xFF03DAC6)
|
||||
val Background = Color(0xFF121212)
|
||||
val Surface = Color(0xFF1E1E1E)
|
||||
val Error = Color(0xFFCF6679)
|
||||
val OnPrimary = Color.White
|
||||
val OnSecondary = Color.Black
|
||||
val OnBackground = Color.White
|
||||
val OnSurface = Color.White
|
||||
val OnError = Color.Black
|
||||
|
||||
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
|
||||
// Статусные цвета
|
||||
val Connected = Color(0xFF4CAF50)
|
||||
val Disconnected = Color(0xFFF44336)
|
||||
val Waiting = Color(0xFFFF9800)
|
||||
}
|
||||
|
||||
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
|
||||
primary = GodEyeColors.Primary,
|
||||
secondary = GodEyeColors.Secondary,
|
||||
tertiary = GodEyeColors.PrimaryVariant,
|
||||
background = GodEyeColors.Background,
|
||||
surface = GodEyeColors.Surface,
|
||||
error = GodEyeColors.Error,
|
||||
onPrimary = GodEyeColors.OnPrimary,
|
||||
onSecondary = GodEyeColors.OnSecondary,
|
||||
onTertiary = GodEyeColors.OnPrimary,
|
||||
onBackground = GodEyeColors.OnBackground,
|
||||
onSurface = GodEyeColors.OnSurface,
|
||||
onError = GodEyeColors.OnError,
|
||||
)
|
||||
|
||||
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
|
||||
primary = GodEyeColors.Primary,
|
||||
secondary = GodEyeColors.Secondary,
|
||||
tertiary = GodEyeColors.PrimaryVariant,
|
||||
background = Color.White,
|
||||
surface = Color(0xFFF5F5F5),
|
||||
error = Color(0xFFD32F2F),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.Black,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color.Black,
|
||||
onSurface = Color.Black,
|
||||
onError = Color.White,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -99,9 +60,10 @@ fun GodEyeTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
val colorScheme = if (darkTheme) {
|
||||
DarkColorScheme
|
||||
} else {
|
||||
LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.example.godeye.utils
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Constants - константы приложения согласно ТЗ
|
||||
*/
|
||||
@@ -8,7 +10,9 @@ object Constants {
|
||||
// Настройки сервера согласно ТЗ
|
||||
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"
|
||||
const val LOCAL_NETWORK_SERVER_URL = "http://192.168.219.108:3001"
|
||||
const val WEBSOCKET_SERVER_URL = "ws://192.168.219.108:3000"
|
||||
const val SIGNALING_SERVER_URL = "ws://192.168.219.108:8765"
|
||||
|
||||
// Настройки Socket.IO
|
||||
const val SOCKET_TIMEOUT = 10000L
|
||||
@@ -53,6 +57,11 @@ object Constants {
|
||||
const val WEBRTC_OFFER_RECEIVED = "webrtc:offer"
|
||||
const val WEBRTC_ANSWER_RECEIVED = "webrtc:answer"
|
||||
const val WEBRTC_ICE_RECEIVED = "webrtc:ice-candidate"
|
||||
|
||||
const val CONNECT = "connect"
|
||||
const val DISCONNECT = "disconnect"
|
||||
const val DEVICE_REGISTER = "device-register"
|
||||
const val KEEP_ALIVE = "keep-alive"
|
||||
}
|
||||
|
||||
// SharedPreferences ключи согласно ТЗ
|
||||
@@ -88,4 +97,37 @@ object Constants {
|
||||
const val FPS_30 = 30
|
||||
const val FPS_60 = 60
|
||||
}
|
||||
|
||||
// НОВОЕ: Добавляем автоматическое определение IP сервера
|
||||
fun getServerUrlForCurrentNetwork(context: Context): String {
|
||||
val localIP = context.getLocalIpAddress()
|
||||
// Проверяем, находимся ли мы в подсети 192.168.219.x
|
||||
return if (localIP.startsWith("192.168.219.")) {
|
||||
"http://192.168.219.108:3001" // Ваш сервер
|
||||
} else {
|
||||
// Fallback для других сетей
|
||||
val serverIP = localIP.substringBeforeLast(".") + ".108"
|
||||
"http://$serverIP:3001"
|
||||
}
|
||||
}
|
||||
|
||||
fun getWebSocketUrlForCurrentNetwork(context: Context): String {
|
||||
val localIP = context.getLocalIpAddress()
|
||||
return if (localIP.startsWith("192.168.219.")) {
|
||||
"ws://192.168.219.108:3000" // Ваш сервер
|
||||
} else {
|
||||
val serverIP = localIP.substringBeforeLast(".") + ".108"
|
||||
"ws://$serverIP:3000"
|
||||
}
|
||||
}
|
||||
|
||||
fun getSignalingUrlForCurrentNetwork(context: Context): String {
|
||||
val localIP = context.getLocalIpAddress()
|
||||
return if (localIP.startsWith("192.168.219.")) {
|
||||
"ws://192.168.219.108:8765" // Ваш сервер
|
||||
} else {
|
||||
val serverIP = localIP.substringBeforeLast(".") + ".108"
|
||||
"ws://$serverIP:8765"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,14 @@
|
||||
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 handle(error: Throwable) {
|
||||
// Простая обработка ошибок: логирование
|
||||
println("Error: ${error.message}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Специальная обработка исключений для предотвращения крашей
|
||||
*/
|
||||
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()
|
||||
}
|
||||
fun handleUncaughtException(thread: Thread, exception: Throwable) {
|
||||
// Обработка неперехваченных исключений
|
||||
println("Uncaught exception in thread ${thread.name}: ${exception.message}")
|
||||
exception.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ package com.example.godeye.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import java.net.NetworkInterface
|
||||
import java.net.InetAddress
|
||||
import java.util.*
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
fun Context.getPreferences(): SharedPreferences {
|
||||
return getSharedPreferences("godeye_prefs", Context.MODE_PRIVATE)
|
||||
@@ -11,3 +17,142 @@ fun Context.getPreferences(): SharedPreferences {
|
||||
fun generateDeviceId(): String {
|
||||
return "android_${UUID.randomUUID().toString().take(8)}"
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Получение реального IP-адреса устройства с обновленными API
|
||||
*/
|
||||
fun Context.getLocalIpAddress(): String {
|
||||
try {
|
||||
// Метод 1: Через WiFi Manager (для WiFi соединений) - обновленный API
|
||||
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
||||
wifiManager?.let { wifi ->
|
||||
if (wifi.isWifiEnabled) {
|
||||
// ИСПРАВЛЕНИЕ: Используем современный API для получения WiFi информации
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
// Для Android 10+ используем ConnectivityManager
|
||||
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
connectivityManager?.let { cm ->
|
||||
val activeNetwork = cm.activeNetwork
|
||||
val linkProperties = cm.getLinkProperties(activeNetwork)
|
||||
linkProperties?.linkAddresses?.forEach { linkAddress ->
|
||||
val address = linkAddress.address
|
||||
if (address is java.net.Inet4Address && !address.isLoopbackAddress) {
|
||||
val ip = address.hostAddress
|
||||
if (ip != null && !ip.startsWith("127.")) {
|
||||
Logger.step("IP_DETECTION", "Modern API IP detected: $ip")
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Для старых версий Android используем deprecated API с обработкой
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiInfo = wifi.connectionInfo
|
||||
@Suppress("DEPRECATION")
|
||||
val ipInt = wifiInfo.ipAddress
|
||||
if (ipInt != 0) {
|
||||
val ip = String.format(
|
||||
"%d.%d.%d.%d",
|
||||
ipInt and 0xff,
|
||||
ipInt shr 8 and 0xff,
|
||||
ipInt shr 16 and 0xff,
|
||||
ipInt shr 24 and 0xff
|
||||
)
|
||||
Logger.step("IP_DETECTION", "Legacy WiFi IP detected: $ip")
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 2: Через NetworkInterface (универсальный метод)
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
for (networkInterface in interfaces) {
|
||||
// Пропускаем loopback и неактивные интерфейсы
|
||||
if (networkInterface.isLoopback || !networkInterface.isUp) continue
|
||||
|
||||
val addresses = networkInterface.inetAddresses
|
||||
for (address in addresses) {
|
||||
// Пропускаем IPv6 и loopback адреса
|
||||
if (!address.isLoopbackAddress && address is java.net.Inet4Address) {
|
||||
val ip = address.hostAddress
|
||||
if (ip != null && !ip.startsWith("127.")) {
|
||||
Logger.step("IP_DETECTION", "Network interface IP detected: $ip on ${networkInterface.name}")
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Метод 3: Через ConnectivityManager (Android 6+)
|
||||
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
connectivityManager?.let { cm ->
|
||||
val activeNetwork = cm.activeNetwork
|
||||
val networkCapabilities = cm.getNetworkCapabilities(activeNetwork)
|
||||
if (networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true) {
|
||||
Logger.step("IP_DETECTION", "Active network detected but couldn't extract IP")
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("IP_DETECTION_ERROR", "Failed to detect local IP", e)
|
||||
}
|
||||
|
||||
// Fallback: возвращаем IP по умолчанию для вашей подсети
|
||||
val fallbackIP = "192.168.219.108" // ИСПРАВЛЕНИЕ: Изменено на ваш сервер
|
||||
Logger.step("IP_DETECTION_FALLBACK", "Using fallback IP: $fallbackIP")
|
||||
return fallbackIP
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Получение IP сервера из URL
|
||||
*/
|
||||
fun extractServerIP(serverUrl: String): String {
|
||||
return try {
|
||||
val regex = """://([^:/]+)""".toRegex()
|
||||
val match = regex.find(serverUrl)
|
||||
match?.groupValues?.get(1) ?: "192.168.219.1" // Fallback на ваш роутер
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SERVER_IP_EXTRACTION_ERROR", "Failed to extract server IP from URL: $serverUrl", e)
|
||||
"192.168.219.1"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Автоматическое определение подсети
|
||||
*/
|
||||
fun Context.detectNetworkSubnet(): String {
|
||||
val localIP = getLocalIpAddress()
|
||||
return try {
|
||||
val parts = localIP.split(".")
|
||||
if (parts.size >= 3) {
|
||||
"${parts[0]}.${parts[1]}.${parts[2]}.0/24"
|
||||
} else {
|
||||
"192.168.219.0/24" // Fallback на вашу подсеть
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SUBNET_DETECTION_ERROR", "Failed to detect subnet", e)
|
||||
"192.168.219.0/24"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* НОВОЕ: Проверка, находится ли IP в локальной сети
|
||||
*/
|
||||
fun isLocalNetworkIP(ip: String): Boolean {
|
||||
return try {
|
||||
val parts = ip.split(".").map { it.toInt() }
|
||||
when {
|
||||
// 192.168.x.x
|
||||
parts[0] == 192 && parts[1] == 168 -> true
|
||||
// 10.x.x.x
|
||||
parts[0] == 10 -> true
|
||||
// 172.16.x.x - 172.31.x.x
|
||||
parts[0] == 172 && parts[1] in 16..31 -> true
|
||||
else -> false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,23 @@ object Logger {
|
||||
throwable?.printStackTrace()
|
||||
}
|
||||
|
||||
// Методы для совместимости с WebRTCManager
|
||||
fun debug(tag: String, message: String) {
|
||||
Log.d(tag, "🔍 $message")
|
||||
println("🔍 [DEBUG] [$tag] $message")
|
||||
}
|
||||
|
||||
fun info(tag: String, message: String) {
|
||||
Log.i(tag, "ℹ️ $message")
|
||||
println("ℹ️ [INFO] [$tag] $message")
|
||||
}
|
||||
|
||||
fun error(tag: String, message: String, throwable: Throwable? = null) {
|
||||
Log.e(tag, "❌ $message", throwable)
|
||||
println("❌ [ERROR] [$tag] $message")
|
||||
throwable?.printStackTrace()
|
||||
}
|
||||
|
||||
fun step(stepName: String, message: String) {
|
||||
Log.d(TAG, "📋 STEP [$stepName]: $message")
|
||||
println("📋 STEP [$stepName]: $message")
|
||||
@@ -55,10 +72,4 @@ object Logger {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.godeye.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* Утилиты для управления разрешениями
|
||||
*/
|
||||
class PermissionHelper(private val activity: ComponentActivity) {
|
||||
|
||||
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.POST_NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
|
||||
private val permissionLauncher = activity.registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
val allGranted = permissions.values.all { it }
|
||||
onPermissionResult?.invoke(allGranted)
|
||||
}
|
||||
|
||||
private var onPermissionResult: ((Boolean) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Проверка всех необходимых разрешений
|
||||
*/
|
||||
fun hasAllPermissions(): Boolean {
|
||||
return REQUIRED_PERMISSIONS.all { permission ->
|
||||
ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос разрешений
|
||||
*/
|
||||
fun requestPermissions(onResult: (Boolean) -> Unit) {
|
||||
onPermissionResult = onResult
|
||||
permissionLauncher.launch(REQUIRED_PERMISSIONS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка конкретного разрешения
|
||||
*/
|
||||
fun hasPermission(permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
185
app/src/main/java/com/example/godeye/utils/PreferenceManager.kt
Normal file
185
app/src/main/java/com/example/godeye/utils/PreferenceManager.kt
Normal file
@@ -0,0 +1,185 @@
|
||||
package com.example.godeye.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Менеджер настроек приложения
|
||||
*/
|
||||
class PreferenceManager(context: Context) {
|
||||
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("godeye_settings", Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PreferenceManager"
|
||||
private const val PREFS_NAME = "godeye_preferences"
|
||||
private const val KEY_DEVICE_ID = "device_id"
|
||||
private const val KEY_SERVER_URL = "server_url"
|
||||
private const val KEY_AUTO_APPROVAL = "auto_approval"
|
||||
private const val KEY_OPERATOR_MODE = "operator_mode"
|
||||
private const val KEY_LAST_CONNECTION = "last_connection"
|
||||
private const val KEY_STATISTICS_ENABLED = "statistics_enabled"
|
||||
private const val KEY_DEVICE_NAME = "device_name"
|
||||
private const val KEY_UUID_MIGRATED = "uuid_migrated"
|
||||
private const val KEY_AUTO_CONNECT = "auto_connect"
|
||||
private const val KEY_AUTO_APPROVE = "auto_approve"
|
||||
private const val KEY_NOTIFICATIONS_ENABLED = "notifications_enabled"
|
||||
private const val KEY_CAMERA_QUALITY = "camera_quality"
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Обновляем IP по умолчанию на ваш сервер
|
||||
private const val DEFAULT_SERVER_URL = "http://192.168.219.108:3001"
|
||||
private const val DEFAULT_DEVICE_NAME = "Android Device"
|
||||
}
|
||||
|
||||
// StateFlow для реактивного наблюдения за изменениями настроек
|
||||
private val _serverUrl = MutableStateFlow(getServerUrl())
|
||||
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
||||
|
||||
private val _deviceName = MutableStateFlow(getDeviceName())
|
||||
val deviceName: StateFlow<String> = _deviceName.asStateFlow()
|
||||
|
||||
private val _autoConnect = MutableStateFlow(getAutoConnect())
|
||||
val autoConnect: StateFlow<Boolean> = _autoConnect.asStateFlow()
|
||||
|
||||
private val _autoApprove = MutableStateFlow(getAutoApprove())
|
||||
val autoApprove: StateFlow<Boolean> = _autoApprove.asStateFlow()
|
||||
|
||||
// URL сервера
|
||||
fun getServerUrl(): String = prefs.getString(KEY_SERVER_URL, DEFAULT_SERVER_URL) ?: DEFAULT_SERVER_URL
|
||||
|
||||
fun setServerUrl(url: String) {
|
||||
prefs.edit().putString(KEY_SERVER_URL, url).apply()
|
||||
_serverUrl.value = url
|
||||
}
|
||||
|
||||
// Имя устройства
|
||||
fun getDeviceName(): String = prefs.getString(KEY_DEVICE_NAME, DEFAULT_DEVICE_NAME) ?: DEFAULT_DEVICE_NAME
|
||||
|
||||
fun setDeviceName(name: String) {
|
||||
prefs.edit().putString(KEY_DEVICE_NAME, name).apply()
|
||||
_deviceName.value = name
|
||||
}
|
||||
|
||||
// Автоматическое подключение
|
||||
fun getAutoConnect(): Boolean = prefs.getBoolean(KEY_AUTO_CONNECT, false)
|
||||
|
||||
fun setAutoConnect(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_AUTO_CONNECT, enabled).apply()
|
||||
_autoConnect.value = enabled
|
||||
}
|
||||
|
||||
// Автоматическое подтверждение
|
||||
fun getAutoApprove(): Boolean = prefs.getBoolean(KEY_AUTO_APPROVE, false)
|
||||
|
||||
fun setAutoApprove(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_AUTO_APPROVE, enabled).apply()
|
||||
_autoApprove.value = enabled
|
||||
}
|
||||
|
||||
// ID устройства (генерируется один раз) - теперь использует UUID
|
||||
fun getDeviceId(): String {
|
||||
var deviceId = prefs.getString(KEY_DEVICE_ID, null)
|
||||
if (deviceId == null) {
|
||||
// Генерируем настоящий UUID вместо timestamp
|
||||
deviceId = java.util.UUID.randomUUID().toString()
|
||||
prefs.edit().putString(KEY_DEVICE_ID, deviceId).apply()
|
||||
}
|
||||
return deviceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительная перегенерация Device ID с новым UUID
|
||||
* Используется для обновления старых устройств на новый формат
|
||||
*/
|
||||
fun regenerateDeviceId(): String {
|
||||
val newDeviceId = java.util.UUID.randomUUID().toString()
|
||||
prefs.edit().putString(KEY_DEVICE_ID, newDeviceId).apply()
|
||||
return newDeviceId
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка, является ли Device ID старым форматом (android_timestamp)
|
||||
*/
|
||||
fun isLegacyDeviceId(): Boolean {
|
||||
val deviceId = prefs.getString(KEY_DEVICE_ID, null)
|
||||
return deviceId != null && deviceId.startsWith("android_")
|
||||
}
|
||||
|
||||
/**
|
||||
* Автоматическое обновление старого Device ID на UUID
|
||||
*/
|
||||
fun migrateToUUID(): String {
|
||||
return if (isLegacyDeviceId()) {
|
||||
val oldDeviceId = prefs.getString(KEY_DEVICE_ID, null)
|
||||
val newDeviceId = regenerateDeviceId()
|
||||
android.util.Log.d("PreferenceManager", "Migrated Device ID from $oldDeviceId to $newDeviceId")
|
||||
newDeviceId
|
||||
} else {
|
||||
getDeviceId()
|
||||
}
|
||||
}
|
||||
|
||||
// Настройки уведомлений
|
||||
fun getNotificationsEnabled(): Boolean = prefs.getBoolean(KEY_NOTIFICATIONS_ENABLED, true)
|
||||
|
||||
fun setNotificationsEnabled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(KEY_NOTIFICATIONS_ENABLED, enabled).apply()
|
||||
}
|
||||
|
||||
// Качество камеры
|
||||
fun getCameraQuality(): String = prefs.getString(KEY_CAMERA_QUALITY, "HIGH") ?: "HIGH"
|
||||
|
||||
fun setCameraQuality(quality: String) {
|
||||
prefs.edit().putString(KEY_CAMERA_QUALITY, quality).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все настройки для отладки
|
||||
*/
|
||||
fun getAllSettings(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"serverUrl" to getServerUrl(),
|
||||
"deviceName" to getDeviceName(),
|
||||
"autoConnect" to getAutoConnect(),
|
||||
"autoApprove" to getAutoApprove(),
|
||||
"deviceId" to getDeviceId(),
|
||||
"notificationsEnabled" to getNotificationsEnabled(),
|
||||
"cameraQuality" to getCameraQuality()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка всех настроек (для отладки)
|
||||
*/
|
||||
fun clearAllSettings() {
|
||||
prefs.edit().clear().apply()
|
||||
android.util.Log.d("PreferenceManager", "All settings cleared")
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка времени последней миграции Device ID
|
||||
*/
|
||||
private fun shouldMigrate(): Boolean {
|
||||
val lastMigration = prefs.getLong("last_migration_time", 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val migrationInterval = 24 * 60 * 60 * 1000L // 24 часа
|
||||
|
||||
return (currentTime - lastMigration) > migrationInterval
|
||||
}
|
||||
|
||||
/**
|
||||
* Автоматическая миграция с проверкой времени
|
||||
*/
|
||||
fun autoMigrateDeviceId(): String {
|
||||
return if (isLegacyDeviceId() && shouldMigrate()) {
|
||||
val newDeviceId = regenerateDeviceId()
|
||||
prefs.edit().putLong("last_migration_time", System.currentTimeMillis()).apply()
|
||||
newDeviceId
|
||||
} else {
|
||||
getDeviceId()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,646 +0,0 @@
|
||||
package com.example.godeye.webrtc
|
||||
|
||||
import android.content.Context
|
||||
import com.example.godeye.utils.Logger
|
||||
import org.webrtc.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* WebRTCManager - обработка WebRTC соединений согласно ТЗ
|
||||
* Архитектура: Socket.IO для сигнализации, WebRTC для P2P медиа-потоков
|
||||
*/
|
||||
class WebRTCManager(
|
||||
private val context: Context,
|
||||
private val onSignalingMessage: (message: JSONObject) -> Unit
|
||||
) {
|
||||
|
||||
private var peerConnectionFactory: PeerConnectionFactory? = null
|
||||
private val activePeerConnections = mutableMapOf<String, PeerConnection>()
|
||||
private var localVideoTrack: VideoTrack? = null
|
||||
private var localAudioTrack: AudioTrack? = null
|
||||
private var videoCapturer: CameraVideoCapturer? = null
|
||||
|
||||
// Состояния соединения
|
||||
private val _connectionState = MutableStateFlow<Map<String, PeerConnection.PeerConnectionState>>(emptyMap())
|
||||
val connectionState: StateFlow<Map<String, PeerConnection.PeerConnectionState>> = _connectionState.asStateFlow()
|
||||
|
||||
// ICE серверы согласно ТЗ
|
||||
private val iceServers = listOf(
|
||||
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(),
|
||||
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()
|
||||
)
|
||||
|
||||
// Конфигурация RTCConfiguration согласно ТЗ
|
||||
private val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||
tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED
|
||||
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
|
||||
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
|
||||
}
|
||||
|
||||
init {
|
||||
Logger.step("WEBRTC_INIT", "Initializing WebRTC Manager according to ТЗ")
|
||||
initializePeerConnectionFactory()
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация PeerConnectionFactory согласно ТЗ
|
||||
*/
|
||||
private fun initializePeerConnectionFactory() {
|
||||
Logger.step("WEBRTC_FACTORY_INIT", "Initializing PeerConnectionFactory")
|
||||
|
||||
try {
|
||||
val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context)
|
||||
.setEnableInternalTracer(false)
|
||||
.createInitializationOptions()
|
||||
PeerConnectionFactory.initialize(initializationOptions)
|
||||
|
||||
val options = PeerConnectionFactory.Options()
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(options)
|
||||
.createPeerConnectionFactory()
|
||||
|
||||
Logger.step("WEBRTC_FACTORY_READY", "PeerConnectionFactory initialized successfully")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_FACTORY_ERROR", "Failed to initialize PeerConnectionFactory", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Начало стриминга для сессии - создание offer согласно ТЗ
|
||||
*/
|
||||
fun startStreaming(sessionId: String, cameraType: String) {
|
||||
Logger.step("WEBRTC_START_STREAMING", "Starting WebRTC streaming for session: $sessionId, camera: $cameraType")
|
||||
|
||||
try {
|
||||
val peerConnection = createPeerConnection(sessionId)
|
||||
if (peerConnection == null) {
|
||||
Logger.error("WEBRTC_START_ERROR", "Failed to create peer connection", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Добавление локальных медиа-потоков
|
||||
addLocalStreams(peerConnection, cameraType)
|
||||
|
||||
// Создание offer согласно ТЗ
|
||||
createOffer(sessionId, peerConnection)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_START_ERROR", "Failed to start WebRTC streaming", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание PeerConnection для сессии
|
||||
*/
|
||||
private fun createPeerConnection(sessionId: String): PeerConnection? {
|
||||
val factory = peerConnectionFactory ?: return null
|
||||
|
||||
val observer = object : PeerConnection.Observer {
|
||||
override fun onIceCandidate(candidate: IceCandidate) {
|
||||
Logger.step("WEBRTC_ICE_CANDIDATE", "ICE candidate for session: $sessionId")
|
||||
|
||||
// Отправка ICE candidate через SocketService согласно ТЗ
|
||||
val message = JSONObject().apply {
|
||||
put("type", "ice-candidate")
|
||||
put("sessionId", sessionId)
|
||||
put("candidate", candidate.sdp)
|
||||
put("sdpMid", candidate.sdpMid)
|
||||
put("sdpMLineIndex", candidate.sdpMLineIndex)
|
||||
}
|
||||
onSignalingMessage(message)
|
||||
}
|
||||
|
||||
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
|
||||
Logger.step("WEBRTC_CONNECTION_CHANGE", "Session $sessionId state: $newState")
|
||||
|
||||
val currentStates = _connectionState.value.toMutableMap()
|
||||
currentStates[sessionId] = newState
|
||||
_connectionState.value = currentStates
|
||||
}
|
||||
|
||||
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||
Logger.step("WEBRTC_ICE_CONNECTION", "Session $sessionId ICE state: $newState")
|
||||
}
|
||||
|
||||
override fun onIceConnectionReceivingChange(receiving: Boolean) {
|
||||
Logger.step("WEBRTC_ICE_RECEIVING", "Session $sessionId ICE receiving: $receiving")
|
||||
}
|
||||
|
||||
override fun onAddStream(stream: MediaStream) {
|
||||
Logger.step("WEBRTC_STREAM_ADDED", "Remote stream added for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onRemoveStream(stream: MediaStream) {
|
||||
Logger.step("WEBRTC_STREAM_REMOVED", "Remote stream removed for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onDataChannel(dataChannel: DataChannel) {
|
||||
Logger.step("WEBRTC_DATA_CHANNEL", "Data channel opened for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onRenegotiationNeeded() {
|
||||
Logger.step("WEBRTC_RENEGOTIATION", "Renegotiation needed for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
|
||||
Logger.step("WEBRTC_ICE_GATHERING", "Session $sessionId ICE gathering: $newState")
|
||||
}
|
||||
|
||||
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
|
||||
Logger.step("WEBRTC_ICE_REMOVED", "ICE candidates removed for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
|
||||
Logger.step("WEBRTC_SIGNALING", "Session $sessionId signaling: $newState")
|
||||
}
|
||||
}
|
||||
|
||||
val peerConnection = factory.createPeerConnection(rtcConfig, observer)
|
||||
if (peerConnection != null) {
|
||||
activePeerConnections[sessionId] = peerConnection
|
||||
}
|
||||
|
||||
return peerConnection
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавление локальных медиа-потоков (видео + аудио)
|
||||
*/
|
||||
private fun addLocalStreams(peerConnection: PeerConnection, cameraType: String) {
|
||||
try {
|
||||
// Создание локального видео-потока
|
||||
if (localVideoTrack == null) {
|
||||
localVideoTrack = createVideoTrack(cameraType)
|
||||
}
|
||||
|
||||
// Создание локального аудио-потока
|
||||
if (localAudioTrack == null) {
|
||||
localAudioTrack = createAudioTrack()
|
||||
}
|
||||
|
||||
// Добавление потоков в PeerConnection
|
||||
localVideoTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
|
||||
localAudioTrack?.let { peerConnection.addTrack(it, listOf("stream")) }
|
||||
|
||||
Logger.step("WEBRTC_STREAMS_ADDED", "Local media streams added to peer connection")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_STREAMS_ERROR", "Failed to add local streams", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание видео-трека с указанной камерой
|
||||
*/
|
||||
private fun createVideoTrack(cameraType: String): VideoTrack? {
|
||||
Logger.step("WEBRTC_VIDEO_TRACK", "Creating video track for camera: $cameraType")
|
||||
|
||||
try {
|
||||
val factory = peerConnectionFactory ?: return null
|
||||
|
||||
// Создание видео источника
|
||||
val videoSource = factory.createVideoSource(false)
|
||||
|
||||
// Создание захватчика камеры
|
||||
videoCapturer = createCameraCapturer(cameraType)
|
||||
|
||||
if (videoCapturer == null) {
|
||||
Logger.error("WEBRTC_VIDEO_ERROR", "Failed to create camera capturer", null)
|
||||
return null
|
||||
}
|
||||
|
||||
// Инициализация захвата видео
|
||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", null)
|
||||
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||||
|
||||
// Запуск захвата видео с разрешением 1280x720 и 30 FPS
|
||||
videoCapturer?.startCapture(1280, 720, 30)
|
||||
|
||||
// Создание видео-трека
|
||||
val videoTrack = factory.createVideoTrack("video_track", videoSource)
|
||||
|
||||
Logger.step("WEBRTC_VIDEO_TRACK_CREATED", "Video track created successfully for camera: $cameraType")
|
||||
return videoTrack
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_VIDEO_TRACK_ERROR", "Failed to create video track", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание захватчика камеры для указанного типа
|
||||
*/
|
||||
private fun createCameraCapturer(cameraType: String): CameraVideoCapturer? {
|
||||
try {
|
||||
val camera1Enumerator = Camera1Enumerator(false)
|
||||
val camera2Enumerator = Camera2Enumerator(context)
|
||||
|
||||
val enumerator = if (Camera2Enumerator.isSupported(context)) camera2Enumerator else camera1Enumerator
|
||||
|
||||
// Поиск камеры по типу
|
||||
val deviceNames = enumerator.deviceNames
|
||||
for (deviceName in deviceNames) {
|
||||
val isFrontFacing = enumerator.isFrontFacing(deviceName)
|
||||
val isBackFacing = enumerator.isBackFacing(deviceName)
|
||||
|
||||
val matches = when (cameraType) {
|
||||
"front" -> isFrontFacing
|
||||
"back", "ultra_wide", "telephoto" -> isBackFacing
|
||||
else -> isBackFacing
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
Logger.d("Using camera device: $deviceName for type: $cameraType")
|
||||
return enumerator.createCapturer(deviceName, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback к первой доступной камере
|
||||
if (deviceNames.isNotEmpty()) {
|
||||
Logger.d("Using fallback camera: ${deviceNames[0]}")
|
||||
return enumerator.createCapturer(deviceNames[0], null)
|
||||
}
|
||||
|
||||
Logger.error("CAMERA_CAPTURER_ERROR", "No camera devices found", null)
|
||||
return null
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("CAMERA_CAPTURER_ERROR", "Failed to create camera capturer", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание аудио-трека
|
||||
*/
|
||||
private fun createAudioTrack(): AudioTrack? {
|
||||
try {
|
||||
val factory = peerConnectionFactory ?: return null
|
||||
|
||||
// Создание аудио источника
|
||||
val audioConstraints = MediaConstraints().apply {
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true"))
|
||||
}
|
||||
|
||||
val audioSource = factory.createAudioSource(audioConstraints)
|
||||
val audioTrack = factory.createAudioTrack("audio_track", audioSource)
|
||||
|
||||
Logger.step("WEBRTC_AUDIO_TRACK_CREATED", "Audio track created successfully")
|
||||
return audioTrack
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_AUDIO_TRACK_ERROR", "Failed to create audio track", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание offer для сессии
|
||||
*/
|
||||
private fun createOffer(sessionId: String, peerConnection: PeerConnection) {
|
||||
try {
|
||||
val constraints = MediaConstraints().apply {
|
||||
// Исправляем настройки для корректной работы WebRTC
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"))
|
||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false"))
|
||||
// Добавляем дополнительные constraints для стабильности
|
||||
mandatory.add(MediaConstraints.KeyValuePair("VoiceActivityDetection", "true"))
|
||||
}
|
||||
|
||||
peerConnection.createOffer(object : SdpObserver {
|
||||
override fun onCreateSuccess(sessionDescription: SessionDescription) {
|
||||
Logger.step("WEBRTC_OFFER_CREATED", "Offer created for session: $sessionId")
|
||||
|
||||
// Изменяем SDP для исправления проблемы с m-section
|
||||
val modifiedSdp = modifySdpForCompatibility(sessionDescription.description)
|
||||
val modifiedSessionDescription = SessionDescription(sessionDescription.type, modifiedSdp)
|
||||
|
||||
peerConnection.setLocalDescription(object : SdpObserver {
|
||||
override fun onSetSuccess() {
|
||||
Logger.step("WEBRTC_LOCAL_DESC_SET", "Local description set successfully for session: $sessionId")
|
||||
|
||||
// Отправка offer через SocketService согласно ТЗ
|
||||
val message = JSONObject().apply {
|
||||
put("type", "offer")
|
||||
put("sessionId", sessionId)
|
||||
put("sdp", modifiedSdp)
|
||||
}
|
||||
onSignalingMessage(message)
|
||||
}
|
||||
|
||||
override fun onSetFailure(error: String) {
|
||||
Logger.error("WEBRTC_SET_LOCAL_ERROR", "Failed to set local description: $error", null)
|
||||
// Не крашим приложение, а пытаемся создать новое соединение
|
||||
handleWebRTCError(sessionId, "Local description error: $error")
|
||||
}
|
||||
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||
override fun onCreateFailure(p0: String?) {}
|
||||
}, modifiedSessionDescription)
|
||||
}
|
||||
|
||||
override fun onCreateFailure(error: String) {
|
||||
Logger.error("WEBRTC_OFFER_ERROR", "Failed to create offer: $error", null)
|
||||
handleWebRTCError(sessionId, "Offer creation error: $error")
|
||||
}
|
||||
|
||||
override fun onSetSuccess() {}
|
||||
override fun onSetFailure(error: String?) {}
|
||||
}, constraints)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_OFFER_EXCEPTION", "Exception creating offer", e)
|
||||
handleWebRTCError(sessionId, "Offer exception: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Модификация SDP для совместимости
|
||||
*/
|
||||
private fun modifySdpForCompatibility(originalSdp: String): String {
|
||||
try {
|
||||
Logger.d("Original SDP length: ${originalSdp.length}")
|
||||
|
||||
// Для send-only режима создаем минимальный корректный SDP
|
||||
val lines = originalSdp.split("\r\n").toMutableList()
|
||||
val modifiedLines = mutableListOf<String>()
|
||||
|
||||
var inVideoSection = false
|
||||
var inAudioSection = false
|
||||
var currentSection = ""
|
||||
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.startsWith("v=") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("o=") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("s=") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("t=") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("a=group:BUNDLE") -> {
|
||||
modifiedLines.add("a=group:BUNDLE 0 1")
|
||||
}
|
||||
line.startsWith("a=msid-semantic") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("m=video") -> {
|
||||
inVideoSection = true
|
||||
inAudioSection = false
|
||||
currentSection = "video"
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("m=audio") -> {
|
||||
inVideoSection = false
|
||||
inAudioSection = true
|
||||
currentSection = "audio"
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("c=") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("a=mid:") -> {
|
||||
when (currentSection) {
|
||||
"video" -> modifiedLines.add("a=mid:0")
|
||||
"audio" -> modifiedLines.add("a=mid:1")
|
||||
else -> modifiedLines.add(line)
|
||||
}
|
||||
}
|
||||
line.startsWith("a=sendonly") || line.startsWith("a=sendrecv") || line.startsWith("a=recvonly") -> {
|
||||
modifiedLines.add("a=sendonly")
|
||||
}
|
||||
line.startsWith("a=rtcp-mux") && !line.contains("only") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
line.startsWith("a=rtpmap:") ||
|
||||
line.startsWith("a=fmtp:") ||
|
||||
line.startsWith("a=ssrc:") ||
|
||||
line.startsWith("a=msid:") ||
|
||||
line.startsWith("a=cname:") ||
|
||||
line.startsWith("a=ice-ufrag:") ||
|
||||
line.startsWith("a=ice-pwd:") ||
|
||||
line.startsWith("a=fingerprint:") ||
|
||||
line.startsWith("a=setup:") ||
|
||||
line.startsWith("a=candidate:") -> {
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
// Пропускаем проблемные RTCP feedback атрибуты для send-only
|
||||
line.startsWith("a=rtcp-fb:") ||
|
||||
line.startsWith("a=rtcp-mux-only") ||
|
||||
line.startsWith("a=rtcp-rsize") -> {
|
||||
// Пропускаем эти строки
|
||||
}
|
||||
line.trim().isEmpty() -> {
|
||||
// Пропускаем пустые строки
|
||||
}
|
||||
else -> {
|
||||
// Добавляем остальные атрибуты
|
||||
modifiedLines.add(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val modifiedSdp = modifiedLines.joinToString("\r\n")
|
||||
|
||||
Logger.d("Modified SDP length: ${modifiedSdp.length}")
|
||||
Logger.d("SDP modifications applied successfully")
|
||||
|
||||
return modifiedSdp
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("SDP_MODIFY_ERROR", "Failed to modify SDP", e)
|
||||
return originalSdp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка WebRTC ошибок без краша приложения
|
||||
*/
|
||||
private fun handleWebRTCError(sessionId: String, error: String) {
|
||||
Logger.error("WEBRTC_ERROR_HANDLED", "WebRTC error for session $sessionId: $error", null)
|
||||
|
||||
// Уведомляем о проблеме через сигналинг
|
||||
val errorMessage = JSONObject().apply {
|
||||
put("type", "error")
|
||||
put("sessionId", sessionId)
|
||||
put("error", error)
|
||||
}
|
||||
|
||||
try {
|
||||
onSignalingMessage(errorMessage)
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_ERROR_SIGNALING", "Failed to send error message", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка answer от оператора
|
||||
*/
|
||||
fun handleAnswer(sessionId: String, answerSdp: String) {
|
||||
Logger.step("WEBRTC_ANSWER", "Processing WebRTC answer for session: $sessionId")
|
||||
|
||||
val peerConnection = activePeerConnections[sessionId]
|
||||
if (peerConnection == null) {
|
||||
Logger.error("WEBRTC_ANSWER_ERROR", "No peer connection found for session: $sessionId", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val answer = SessionDescription(SessionDescription.Type.ANSWER, answerSdp)
|
||||
peerConnection.setRemoteDescription(object : SdpObserver {
|
||||
override fun onSetSuccess() {
|
||||
Logger.step("WEBRTC_ANSWER_SET", "Answer set successfully for session: $sessionId")
|
||||
}
|
||||
|
||||
override fun onSetFailure(error: String) {
|
||||
Logger.error("WEBRTC_ANSWER_SET_ERROR", "Failed to set answer: $error", null)
|
||||
}
|
||||
|
||||
override fun onCreateSuccess(p0: SessionDescription?) {}
|
||||
override fun onCreateFailure(p0: String?) {}
|
||||
}, answer)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_ANSWER_EXCEPTION", "Exception handling answer", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка offer от оператора (не используется в текущей архитектуре)
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun handleOffer(sessionId: String, offerSdp: String) {
|
||||
Logger.step("WEBRTC_OFFER", "Processing WebRTC offer for session: $sessionId")
|
||||
// Пока не реализовано - Android устройство только отправляет offer
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка ICE кандидата от оператора
|
||||
*/
|
||||
fun handleIceCandidate(sessionId: String, candidateSdp: String, sdpMid: String, sdpMLineIndex: Int) {
|
||||
Logger.step("WEBRTC_ICE", "Processing ICE candidate for session: $sessionId")
|
||||
|
||||
val peerConnection = activePeerConnections[sessionId]
|
||||
if (peerConnection == null) {
|
||||
Logger.error("WEBRTC_ICE_ERROR", "No peer connection found for session: $sessionId", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val iceCandidate = IceCandidate(sdpMid, sdpMLineIndex, candidateSdp)
|
||||
peerConnection.addIceCandidate(iceCandidate)
|
||||
Logger.step("WEBRTC_ICE_ADDED", "ICE candidate added for session: $sessionId")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_ICE_EXCEPTION", "Exception handling ICE candidate", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключение камеры
|
||||
*/
|
||||
fun switchCamera(cameraType: String) {
|
||||
Logger.step("WEBRTC_SWITCH_CAMERA", "Switching camera to: $cameraType")
|
||||
|
||||
try {
|
||||
// Остановка текущего захвата
|
||||
videoCapturer?.stopCapture()
|
||||
|
||||
// Создание нового видео-трека
|
||||
localVideoTrack?.dispose()
|
||||
localVideoTrack = createVideoTrack(cameraType)
|
||||
|
||||
// Обновление треков во всех активных соединениях
|
||||
activePeerConnections.forEach { (_, peerConnection) ->
|
||||
localVideoTrack?.let { videoTrack ->
|
||||
// Удаление старого трека и добавление нового
|
||||
val senders = peerConnection.senders
|
||||
senders.forEach { sender ->
|
||||
if (sender.track()?.kind() == "video") {
|
||||
sender.setTrack(videoTrack, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.step("WEBRTC_CAMERA_SWITCHED", "Camera switched to: $cameraType")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.error("WEBRTC_SWITCH_ERROR", "Failed to switch camera", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение сессии
|
||||
*/
|
||||
fun endSession(sessionId: String) {
|
||||
Logger.step("WEBRTC_END_SESSION", "Ending WebRTC session: $sessionId")
|
||||
|
||||
activePeerConnections[sessionId]?.let { peerConnection ->
|
||||
peerConnection.close()
|
||||
activePeerConnections.remove(sessionId)
|
||||
}
|
||||
|
||||
val currentStates = _connectionState.value.toMutableMap()
|
||||
currentStates.remove(sessionId)
|
||||
_connectionState.value = currentStates
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка всех стримов
|
||||
*/
|
||||
fun stopAllStreaming() {
|
||||
Logger.step("WEBRTC_STOP_ALL", "Stopping all WebRTC streaming")
|
||||
|
||||
activePeerConnections.forEach { (_, peerConnection) ->
|
||||
peerConnection.close()
|
||||
}
|
||||
activePeerConnections.clear()
|
||||
|
||||
videoCapturer?.stopCapture()
|
||||
videoCapturer?.dispose()
|
||||
videoCapturer = null
|
||||
|
||||
localVideoTrack?.dispose()
|
||||
localVideoTrack = null
|
||||
|
||||
localAudioTrack?.dispose()
|
||||
localAudioTrack = null
|
||||
|
||||
_connectionState.value = emptyMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождение ресурсов
|
||||
*/
|
||||
fun dispose() {
|
||||
Logger.step("WEBRTC_DISPOSE", "Disposing WebRTC Manager")
|
||||
stopAllStreaming()
|
||||
peerConnectionFactory?.dispose()
|
||||
peerConnectionFactory = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SdpObserver по умолчанию для упрощения кода
|
||||
*/
|
||||
open class SimpleSdpObserver : SdpObserver {
|
||||
override fun onCreateSuccess(sessionDescription: SessionDescription) {}
|
||||
override fun onSetSuccess() {}
|
||||
override fun onCreateFailure(error: String) {}
|
||||
override fun onSetFailure(error: String) {}
|
||||
}
|
||||
24
app/src/main/res/drawable/app_background.xml
Normal file
24
app/src/main/res/drawable/app_background.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Основной градиентный фон в стиле GodEye -->
|
||||
<item>
|
||||
<shape>
|
||||
<gradient
|
||||
android:startColor="#0a1828"
|
||||
android:centerColor="#1e3a5f"
|
||||
android:endColor="#2d5a87"
|
||||
android:angle="135" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Дополнительный слой с точечным паттерном -->
|
||||
<item>
|
||||
<shape>
|
||||
<gradient
|
||||
android:startColor="#00000000"
|
||||
android:centerColor="#1a000000"
|
||||
android:endColor="#33000000"
|
||||
android:angle="45" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
22
app/src/main/res/drawable/button_danger.xml
Normal file
22
app/src/main/res/drawable/button_danger.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_danger_pressed" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_secondary" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_danger" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
22
app/src/main/res/drawable/button_primary.xml
Normal file
22
app/src/main/res/drawable/button_primary.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_primary_pressed" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_secondary" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_primary" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
25
app/src/main/res/drawable/button_secondary.xml
Normal file
25
app/src/main/res/drawable/button_secondary.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_secondary_pressed" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="@color/panel_border" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_enabled="false">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#404040" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="@color/panel_border" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/button_secondary" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke android:width="1dp" android:color="@color/panel_border" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
9
app/src/main/res/drawable/circle_indicator.xml
Normal file
9
app/src/main/res/drawable/circle_indicator.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/status_idle" />
|
||||
<size
|
||||
android:width="20dp"
|
||||
android:height="20dp" />
|
||||
</shape>
|
||||
|
||||
15
app/src/main/res/drawable/ic_camera_active.xml
Normal file
15
app/src/main/res/drawable/ic_camera_active.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!-- Основная иконка камеры -->
|
||||
<path
|
||||
android:fillColor="@color/success_green"
|
||||
android:pathData="M17,10.5V7A1,1 0,0 0,16 6H4A1,1 0,0 0,3 7V17A1,1 0,0 0,4 18H16A1,1 0,0 0,17 17V13.5L21,17.5V6.5L17,10.5Z"/>
|
||||
<!-- Красный индикатор записи -->
|
||||
<path
|
||||
android:fillColor="@color/error_red"
|
||||
android:pathData="M19,2A3,3 0,1 1,19 8A3,3 0,1 1,19 2Z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_camera_request.xml
Normal file
11
app/src/main/res/drawable/ic_camera_request.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/accent_blue"
|
||||
android:pathData="M9,12l2,2l4,-4m5.618,-4.016A11.955,11.955 0,0 1,12 2.944A11.955,11.955 0,0 1,2.382 5.984l1.158,1.158a10,10 0,0 0,17.8 0L20.618,5.984zM21,10v8a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2,-2V10l9,-5 9,5z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,170 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- Основной градиентный фон -->
|
||||
<path android:pathData="M0,0h108v108h-108z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="0"
|
||||
android:startX="0"
|
||||
android:endY="108"
|
||||
android:endX="108"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#0A1828"/>
|
||||
<item android:offset="0.3" android:color="#1E3A5F"/>
|
||||
<item android:offset="0.7" android:color="#2D5A87"/>
|
||||
<item android:offset="1" android:color="#3A6EA5"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
||||
<!-- Дополнительные элементы фона -->
|
||||
<!-- Круговые линии для технологического эффекта (заменены на path) -->
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
android:pathData="M54,14A40,40 0,1 1,54,94A40,40 0,1 1,54,14Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1A40E0D0"
|
||||
android:strokeWidth="1"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
android:pathData="M54,4A50,50 0,1 1,54,104A50,50 0,1 1,54,4Z"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1A00BFFF"
|
||||
android:strokeWidth="0.5"/>
|
||||
|
||||
<!-- Точки на фоне (заменены на path) -->
|
||||
<path android:pathData="M20,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
|
||||
<path android:pathData="M88,19a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
|
||||
<path android:pathData="M20,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
|
||||
<path android:pathData="M88,87a1,1 0,1 1,-2,0a1,1 0,1 1,2 0z" android:fillColor="#3340E0D0"/>
|
||||
</vector>
|
||||
|
||||
@@ -4,27 +4,80 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
<!-- Фоновый градиент -->
|
||||
<path
|
||||
android:pathData="M0,0h108v108h-108z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startY="0"
|
||||
android:startX="0"
|
||||
android:endY="108"
|
||||
android:endX="108"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#0A1828"/>
|
||||
<item android:offset="0.5" android:color="#1E3A5F"/>
|
||||
<item android:offset="1" android:color="#2D5A87"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
||||
<!-- Основной контур глаза -->
|
||||
<path
|
||||
android:fillColor="#00BFFF"
|
||||
android:pathData="M54,30C42,30 32,40 32,52C32,64 42,74 54,74C66,74 76,64 76,52C76,40 66,30 54,30Z"/>
|
||||
|
||||
<!-- Внутренний контур глаза -->
|
||||
<path
|
||||
android:fillColor="#E0F4FF"
|
||||
android:pathData="M54,35C45,35 37,43 37,52C37,61 45,69 54,69C63,69 71,61 71,52C71,43 63,35 54,35Z"/>
|
||||
|
||||
<!-- Зрачок -->
|
||||
<path
|
||||
android:fillColor="#0A1828"
|
||||
android:pathData="M54,42C50,42 47,45 47,49C47,53 50,56 54,56C58,56 61,53 61,49C61,45 58,42 54,42Z"/>
|
||||
|
||||
<!-- Отражение в глазу -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M52,44C51,44 50,45 50,46C50,47 51,48 52,48C53,48 54,47 54,46C54,45 53,44 52,44Z"/>
|
||||
|
||||
<!-- Технологические линии вокруг глаза -->
|
||||
<!-- Верхние линии -->
|
||||
<path
|
||||
android:fillColor="#40E0D0"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#40E0D0"
|
||||
android:pathData="M25,45 L30,45 M78,45 L83,45"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#40E0D0"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#40E0D0"
|
||||
android:pathData="M25,52 L30,52 M78,52 L83,52"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#40E0D0"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#40E0D0"
|
||||
android:pathData="M25,59 L30,59 M78,59 L83,59"/>
|
||||
|
||||
<!-- Точки на концах линий (заменены на path элементы) -->
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M23,43a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M23,50a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M23,57a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M85,43a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M85,50a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
<path
|
||||
android:fillColor="#00FFFF"
|
||||
android:pathData="M85,57a2,2 0,1 1,-4,0a2,2 0,1 1,4 0z"/>
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/ic_recording_active.xml
Normal file
14
app/src/main/res/drawable/ic_recording_active.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/error_red"
|
||||
android:pathData="M12,12m-8,0a8,8 0,1 1,16 0a8,8 0,1 1,-16 0" />
|
||||
<path
|
||||
android:fillColor="@color/white"
|
||||
android:pathData="M12,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
|
||||
</vector>
|
||||
|
||||
7
app/src/main/res/drawable/info_panel_background.xml
Normal file
7
app/src/main/res/drawable/info_panel_background.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/panel_background" />
|
||||
<corners android:radius="12dp" />
|
||||
<stroke android:width="1dp" android:color="@color/panel_border" />
|
||||
</shape>
|
||||
256
app/src/main/res/layout/activity_streaming_monitor.xml
Normal file
256
app/src/main/res/layout/activity_streaming_monitor.xml
Normal file
@@ -0,0 +1,256 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@color/background_dark">
|
||||
|
||||
<!-- Заголовок -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Мониторинг трансляции"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<!-- Статус соединения -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Соединение:"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connectionStatusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Не подключено"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/error_red" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Индикатор статуса трансляции -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Статус трансляции:"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<View
|
||||
android:id="@+id/streamingStatusIndicator"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/circle_indicator"
|
||||
android:backgroundTint="@color/status_idle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statusText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Готов к трансляции"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Время записи -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Время записи:"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/recordingIndicator"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_recording_active"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recordingTimeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="00:00:00"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"
|
||||
android:textColor="@color/accent_blue" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Разделитель -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<!-- Кнопки управления -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<!-- Первая строка кнопок -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/startButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Начать трансляцию"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:background="@drawable/button_primary"
|
||||
android:textColor="@color/white"
|
||||
android:elevation="4dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stopButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Остановить"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:background="@drawable/button_danger"
|
||||
android:textColor="@color/white"
|
||||
android:elevation="4dp"
|
||||
android:enabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Вторая строка кнопок -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/switchCameraButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="Переключить камеру"
|
||||
android:textSize="12sp"
|
||||
android:background="@drawable/button_secondary"
|
||||
android:textColor="@color/text_primary"
|
||||
android:elevation="2dp"
|
||||
android:enabled="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/disconnectButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="Отключиться"
|
||||
android:textSize="12sp"
|
||||
android:background="@drawable/button_secondary"
|
||||
android:textColor="@color/text_primary"
|
||||
android:elevation="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Информация о сессии -->
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/info_panel_background"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Информация о сессии"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionInfoText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Сессия не активна"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Возможности:"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="• Видеопоток без предварительного просмотра\n• Счетчик времени записи\n• Переключение камер\n• Статус трансляции в реальном времени"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Оригинальные цвета Material Design -->
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
@@ -7,4 +8,67 @@
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- Цветовая схема GodEye Signal Center -->
|
||||
<!-- Основные цвета из предоставленных изображений -->
|
||||
<color name="godeye_primary">#00BFFF</color> <!-- Яркий голубой/cyan -->
|
||||
<color name="godeye_primary_variant">#0080FF</color> <!-- Темно-голубой -->
|
||||
<color name="godeye_secondary">#40E0D0</color> <!-- Бирюзовый -->
|
||||
<color name="godeye_accent">#00FFFF</color> <!-- Cyan accent -->
|
||||
|
||||
<!-- Фоновые цвета -->
|
||||
<color name="godeye_background_dark">#0A1828</color> <!-- Темно-синий фон -->
|
||||
<color name="godeye_background_medium">#1E3A5F</color> <!-- Средний синий -->
|
||||
<color name="godeye_background_light">#2D5A87</color> <!-- Светлый синий -->
|
||||
|
||||
<!-- Поверхности и карточки -->
|
||||
<color name="godeye_surface">#1A2B3D</color> <!-- Поверхность карточек -->
|
||||
<color name="godeye_surface_variant">#243447</color> <!-- Вариант поверхности -->
|
||||
<color name="godeye_on_surface">#E0F4FF</color> <!-- Текст на поверхности -->
|
||||
<color name="godeye_on_surface_variant">#B0C4DE</color> <!-- Вторичный текст -->
|
||||
|
||||
<!-- Состояния -->
|
||||
<color name="godeye_success">#00FF80</color> <!-- Успешное подключение -->
|
||||
<color name="godeye_warning">#FFB000</color> <!-- Предупреждения -->
|
||||
<color name="godeye_error">#FF4444</color> <!-- Ошибки -->
|
||||
|
||||
<!-- Прозрачности -->
|
||||
<color name="godeye_overlay_light">#33FFFFFF</color> <!-- Светлая overlay -->
|
||||
<color name="godeye_overlay_dark">#66000000</color> <!-- Темная overlay -->
|
||||
|
||||
<!-- Специальные эффекты -->
|
||||
<color name="godeye_glow">#80BFFF</color> <!-- Свечение элементов -->
|
||||
<color name="godeye_connection_active">#00FF7F</color> <!-- Активное соединение -->
|
||||
<color name="godeye_connection_inactive">#778899</color> <!-- Неактивное соединение -->
|
||||
|
||||
<!-- Основные цвета интерфейса мониторинга трансляции -->
|
||||
<color name="background_dark">#1E1E1E</color>
|
||||
<color name="text_primary">#FFFFFF</color>
|
||||
<color name="text_secondary">#B3B3B3</color>
|
||||
<color name="divider">#333333</color>
|
||||
|
||||
<!-- Цвета статусов трансляции -->
|
||||
<color name="status_idle">#6C757D</color>
|
||||
<color name="status_connecting">#FFC107</color>
|
||||
<color name="status_streaming">#28A745</color>
|
||||
<color name="status_error">#DC3545</color>
|
||||
<color name="status_stopping">#FF8C00</color>
|
||||
|
||||
<!-- Цвета состояний -->
|
||||
<color name="success_green">#28A745</color>
|
||||
<color name="error_red">#DC3545</color>
|
||||
<color name="warning_yellow">#FFC107</color>
|
||||
<color name="accent_blue">#007BFF</color>
|
||||
|
||||
<!-- Цвета кнопок -->
|
||||
<color name="button_primary">#007BFF</color>
|
||||
<color name="button_primary_pressed">#0056B3</color>
|
||||
<color name="button_danger">#DC3545</color>
|
||||
<color name="button_danger_pressed">#C82333</color>
|
||||
<color name="button_secondary">#6C757D</color>
|
||||
<color name="button_secondary_pressed">#5A6268</color>
|
||||
|
||||
<!-- Цвета панелей -->
|
||||
<color name="panel_background">#2D2D2D</color>
|
||||
<color name="panel_border">#404040</color>
|
||||
</resources>
|
||||
@@ -1,7 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.GodEye" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="Theme.GodEye" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Основные цвета приложения -->
|
||||
<item name="colorPrimary">@color/godeye_primary</item>
|
||||
<item name="colorPrimaryVariant">@color/godeye_primary_variant</item>
|
||||
<item name="colorSecondary">@color/godeye_secondary</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
|
||||
<!-- Фон приложения -->
|
||||
<item name="android:windowBackground">@drawable/app_background</item>
|
||||
<item name="colorSurface">@color/godeye_surface</item>
|
||||
<item name="colorOnSurface">@color/godeye_on_surface</item>
|
||||
|
||||
<!-- Статус бар -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
|
||||
<!-- Навигационная панель -->
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
||||
<!-- Дополнительные настройки -->
|
||||
<item name="android:windowContentTransitions">true</item>
|
||||
<item name="android:windowAllowEnterTransitionOverlap">true</item>
|
||||
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Тема для экрана заставки -->
|
||||
<style name="Theme.GodEye.Splash" parent="Theme.GodEye">
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
112
app/src/ui.disabled.backup/theme/GodEyeTheme.kt
Normal file
112
app/src/ui.disabled.backup/theme/GodEyeTheme.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.example.godeye.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Цветовая палитра GodEye согласно ТЗ
|
||||
*/
|
||||
object GodEyeColors {
|
||||
// Основные цвета приложения
|
||||
val BlackPure = Color(0xFF000000)
|
||||
val BlackSoft = Color(0xFF1A1A1A)
|
||||
val BlackMedium = Color(0xFF2D2D2D)
|
||||
|
||||
val IvoryPure = Color(0xFFFFFFF0)
|
||||
val IvorySoft = Color(0xFFF5F5DC)
|
||||
val IvoryMedium = Color(0xFFE6E6D4)
|
||||
|
||||
val NavyDark = Color(0xFF0F1419)
|
||||
val NavyMedium = Color(0xFF1E2328)
|
||||
val NavyLight = Color(0xFF2D3748)
|
||||
|
||||
// Функциональные цвета
|
||||
val RecordRed = Color(0xFFFF3B30)
|
||||
val WarningAmber = Color(0xFFFF9500)
|
||||
val SuccessGreen = Color(0xFF30D158)
|
||||
val InfoBlue = Color(0xFF007AFF)
|
||||
|
||||
// Градиенты
|
||||
val PrimaryGradientStart = NavyDark
|
||||
val PrimaryGradientEnd = BlackSoft
|
||||
|
||||
val AccentGradientStart = NavyLight
|
||||
val AccentGradientEnd = NavyMedium
|
||||
}
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = GodEyeColors.NavyLight,
|
||||
onPrimary = GodEyeColors.IvoryPure,
|
||||
primaryContainer = GodEyeColors.NavyMedium,
|
||||
onPrimaryContainer = GodEyeColors.IvorySoft,
|
||||
|
||||
secondary = GodEyeColors.IvoryMedium,
|
||||
onSecondary = GodEyeColors.BlackPure,
|
||||
secondaryContainer = GodEyeColors.BlackMedium,
|
||||
onSecondaryContainer = GodEyeColors.IvoryPure,
|
||||
|
||||
tertiary = GodEyeColors.WarningAmber,
|
||||
onTertiary = GodEyeColors.BlackPure,
|
||||
|
||||
error = GodEyeColors.RecordRed,
|
||||
onError = GodEyeColors.IvoryPure,
|
||||
|
||||
background = GodEyeColors.BlackPure,
|
||||
onBackground = GodEyeColors.IvoryPure,
|
||||
|
||||
surface = GodEyeColors.BlackSoft,
|
||||
onSurface = GodEyeColors.IvoryPure,
|
||||
surfaceVariant = GodEyeColors.BlackMedium,
|
||||
onSurfaceVariant = GodEyeColors.IvorySoft,
|
||||
|
||||
outline = GodEyeColors.NavyMedium,
|
||||
outlineVariant = GodEyeColors.NavyLight
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = GodEyeColors.NavyMedium,
|
||||
onPrimary = GodEyeColors.IvoryPure,
|
||||
primaryContainer = GodEyeColors.NavyLight,
|
||||
onPrimaryContainer = GodEyeColors.BlackPure,
|
||||
|
||||
secondary = GodEyeColors.BlackMedium,
|
||||
onSecondary = GodEyeColors.IvoryPure,
|
||||
secondaryContainer = GodEyeColors.IvoryMedium,
|
||||
onSecondaryContainer = GodEyeColors.BlackPure,
|
||||
|
||||
tertiary = GodEyeColors.WarningAmber,
|
||||
onTertiary = GodEyeColors.IvoryPure,
|
||||
|
||||
error = GodEyeColors.RecordRed,
|
||||
onError = GodEyeColors.IvoryPure,
|
||||
|
||||
background = GodEyeColors.IvoryPure,
|
||||
onBackground = GodEyeColors.BlackPure,
|
||||
|
||||
surface = GodEyeColors.IvorySoft,
|
||||
onSurface = GodEyeColors.BlackPure,
|
||||
surfaceVariant = GodEyeColors.IvoryMedium,
|
||||
onSurfaceVariant = GodEyeColors.BlackMedium,
|
||||
|
||||
outline = GodEyeColors.NavyLight,
|
||||
outlineVariant = GodEyeColors.NavyMedium
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GodEyeTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
266
build.log
Normal file
266
build.log
Normal file
@@ -0,0 +1,266 @@
|
||||
Configuration on demand is an incubating feature.
|
||||
> Task :app:clean
|
||||
> Task :app:preBuild UP-TO-DATE
|
||||
> Task :app:preDebugBuild UP-TO-DATE
|
||||
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
|
||||
> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
|
||||
> Task :app:dataBindingMergeDependencyArtifactsDebug
|
||||
> Task :app:generateDebugResValues FROM-CACHE
|
||||
> Task :app:generateDebugResources FROM-CACHE
|
||||
> Task :app:mergeDebugResources FROM-CACHE
|
||||
> Task :app:packageDebugResources FROM-CACHE
|
||||
> Task :app:processDebugNavigationResources FROM-CACHE
|
||||
> Task :app:parseDebugLocalResources FROM-CACHE
|
||||
> Task :app:dataBindingGenBaseClassesDebug FROM-CACHE
|
||||
> Task :app:checkDebugAarMetadata
|
||||
> Task :app:compileDebugNavigationResources FROM-CACHE
|
||||
> Task :app:mapDebugSourceSetPaths
|
||||
> Task :app:createDebugCompatibleScreenManifests
|
||||
> Task :app:extractDeepLinksDebug FROM-CACHE
|
||||
> Task :app:processDebugMainManifest FROM-CACHE
|
||||
> Task :app:processDebugManifest FROM-CACHE
|
||||
> Task :app:processDebugManifestForPackage FROM-CACHE
|
||||
> Task :app:processDebugResources FROM-CACHE
|
||||
> Task :app:javaPreCompileDebug FROM-CACHE
|
||||
> Task :app:mergeDebugShaders
|
||||
> Task :app:compileDebugShaders NO-SOURCE
|
||||
> Task :app:generateDebugAssets UP-TO-DATE
|
||||
> Task :app:mergeDebugAssets
|
||||
> Task :app:compressDebugAssets FROM-CACHE
|
||||
> Task :app:desugarDebugFileDependencies FROM-CACHE
|
||||
> Task :app:checkDebugDuplicateClasses
|
||||
> Task :app:mergeDebugJniLibFolders
|
||||
> Task :app:mergeExtDexDebug FROM-CACHE
|
||||
> Task :app:mergeLibDexDebug FROM-CACHE
|
||||
> Task :app:mergeDebugNativeLibs NO-SOURCE
|
||||
> Task :app:stripDebugDebugSymbols NO-SOURCE
|
||||
> Task :app:validateSigningDebug
|
||||
> Task :app:writeDebugAppMetadata
|
||||
> Task :app:writeDebugSigningConfigVersions
|
||||
|
||||
> Task :app:compileDebugKotlin FAILED
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:4:33 Unresolved reference 'ErrorHandler'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/GodEyeApplication.kt:13:32 Unresolved reference 'ErrorHandler'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:12:26 Unresolved reference 'result'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:50:38 Unresolved reference 'registerForActivityResult'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:51:9 Unresolved reference 'ActivityResultContracts'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:52:9 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:38 Unresolved reference 'values'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:49 Unresolved reference 'it'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/LegacyMainActivity.kt:53:51 Unresolved reference 'it'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:18:27 Unresolved reference 'webrtc'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:38:32 Unresolved reference 'WebRTCManager'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.ConnectionState>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:141:47 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:36 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:44 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.CameraRequest?>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:149:46 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:152:56 Unresolved reference 'operatorId'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:152:82 Unresolved reference 'cameraType'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:153:44 Assignment type mismatch: actual type is 'kotlin.Any', but 'com.example.godeye.models.CameraRequest?' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:34 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:42 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.services.WebRTCEvent?>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:160:44 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:24 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:24 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
|
||||
fun <T, R> T.let(block: (T) -> R): R
|
||||
[R|Contract description]
|
||||
<
|
||||
CallsInPlace(block, EXACTLY_ONCE)
|
||||
>
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:161:28 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<kotlin.collections.Map<kotlin.String, com.example.godeye.models.CameraSession>>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:167:47 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:44 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:44 Not enough information to infer type argument for 'V'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:57 Not enough information to infer type argument for 'V'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:168:68 Not enough information to infer type argument for 'V'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:172:46 Unresolved reference 'operatorId'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:173:46 Unresolved reference 'cameraType'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:174:46 Unresolved reference 'webRTCConnected'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:176:60 Unresolved reference 'startTime'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:47 Unresolved reference 'values'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:58 Unresolved reference 'isActive'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:180:60 Unresolved reference 'it'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:189:25 Unresolved reference 'WebRTCManager'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:189:50 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:191:82 Unresolved reference 'getString'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:260:32 Unresolved reference 'startStreaming'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:292:32 Unresolved reference 'handleOffer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:296:32 Unresolved reference 'handleAnswer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:300:32 Unresolved reference 'handleIceCandidate'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:317:32 Unresolved reference 'stopAllStreaming'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:338:32 Unresolved reference 'switchCamera'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:361:24 Unresolved reference 'endSession'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:374:24 Unresolved reference 'stopAllStreaming'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:37 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:45 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.ConnectionState>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:415:47 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:36 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:44 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.models.CameraRequest?>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:422:46 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:34 This is an internal kotlinx.coroutines API that should not be used from outside of kotlinx.coroutines. No compatibility guarantees are provided. It is recommended to report your use-case of internal API to kotlinx.coroutines issue tracker, so stable API could be provided instead
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:42 Argument type mismatch: actual type is 'kotlin.Function1<ERROR CLASS: Unknown return lambda parameter type, ERROR CLASS: Unknown return lambda parameter type>', but 'kotlinx.coroutines.flow.FlowCollector<com.example.godeye.services.WebRTCEvent?>' was expected.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:429:44 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:24 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:24 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
|
||||
fun <T, R> T.let(block: (T) -> R): R
|
||||
[R|Contract description]
|
||||
<
|
||||
CallsInPlace(block, EXACTLY_ONCE)
|
||||
>
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:430:28 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:474:37 Unresolved reference 'WebRTCManager'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:474:62 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:481:32 Unresolved reference 'startStreaming'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/MainViewModel.kt:495:24 Unresolved reference 'dispose'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:38:84 Unresolved reference 'UPSIDE_DOWN_CAKE'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:39:42 Unresolved reference 'FOREGROUND_SERVICE_CAMERA'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/managers/PermissionManager.kt:196:33 Unresolved reference 'FOREGROUND_SERVICE_CAMERA'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:6:12 Unresolved reference 'webrtc'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:7:12 Unresolved reference 'webrtc'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:20:40 Unresolved reference 'PeerConnectionFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:21:33 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:22:34 Unresolved reference 'VideoTrack'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:23:34 Unresolved reference 'AudioTrack'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:24:32 Unresolved reference 'CameraVideoCapturer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:25:39 Unresolved reference 'SurfaceTextureHelper'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:27:36 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:27:53 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:28:36 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:28:91 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:33:30 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:34:9 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:34:9 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:35:9 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:43:27 Unresolved reference 'PeerConnectionFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:46:9 Unresolved reference 'PeerConnectionFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:48:33 Unresolved reference 'JavaAudioDeviceModule'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:50:33 Unresolved reference 'PeerConnectionFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:52:37 Unresolved reference 'DefaultVideoEncoderFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:53:17 Unresolved reference 'EglBase'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:54:37 Unresolved reference 'DefaultVideoDecoderFactory'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:54:64 Unresolved reference 'EglBase'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:78:28 Unresolved reference 'stopCapture'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:79:28 Unresolved reference 'dispose'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:81:30 Unresolved reference 'dispose'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:82:30 Unresolved reference 'dispose'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:84:29 Unresolved reference 'close'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:88:38 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:97:28 Unresolved reference 'CameraVideoCapturer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:97:50 Unresolved reference 'switchCamera'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:22 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:66 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:101:72 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:102:13 Unresolved reference 'bundlePolicy'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:102:28 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:103:13 Unresolved reference 'rtcpMuxPolicy'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:103:29 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:106:49 Unresolved reference 'createPeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:106:87 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:107:13 'onSignalingChange' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:107:51 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:111:13 'onIceConnectionChange' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:111:55 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:115:13 'onConnectionChange' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:115:52 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:120:13 'onIceCandidate' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:120:52 Unresolved reference 'IceCandidate'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:123:48 Unresolved reference 'sdp'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:124:52 Unresolved reference 'sdpMLineIndex'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:125:45 Unresolved reference 'sdpMid'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:130:13 'onIceCandidatesRemoved' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:130:71 Unresolved reference 'IceCandidate'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:131:13 'onAddStream' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:131:46 Unresolved reference 'MediaStream'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:132:13 'onRemoveStream' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:132:49 Unresolved reference 'MediaStream'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:133:13 'onDataChannel' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:133:49 Unresolved reference 'DataChannel'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:134:13 'onRenegotiationNeeded' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:135:13 'onIceGatheringChange' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:135:54 Unresolved reference 'PeerConnection'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:136:13 'onIceConnectionReceivingChange' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:141:50 Unresolved reference 'createVideoSource'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:142:50 Unresolved reference 'createVideoTrack'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:144:50 Unresolved reference 'createAudioSource'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:144:68 Unresolved reference 'MediaConstraints'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:145:50 Unresolved reference 'createAudioTrack'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:147:45 Unresolved reference 'createLocalMediaStream'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:26 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:26 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
|
||||
fun <T, R> T.let(block: (T) -> R): R
|
||||
[R|Contract description]
|
||||
<
|
||||
CallsInPlace(block, EXACTLY_ONCE)
|
||||
>
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:148:30 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:26 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:26 Unresolved reference. None of the following candidates is applicable because of a receiver type mismatch:
|
||||
fun <T, R> T.let(block: (T) -> R): R
|
||||
[R|Contract description]
|
||||
<
|
||||
CallsInPlace(block, EXACTLY_ONCE)
|
||||
>
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:149:30 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:17 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:21 Cannot infer type for this parameter. Please specify it explicitly.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:151:39 Unresolved reference 'addStream'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:156:47 Unresolved reference 'VideoSource'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:157:32 Unresolved reference 'Camera2Enumerator'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:159:47 Unresolved reference 'isFrontFacing'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:159:80 Unresolved reference 'it'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:161:47 Unresolved reference 'isBackFacing'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:161:79 Unresolved reference 'it'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:165:36 Unresolved reference 'SurfaceTextureHelper'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:165:80 Unresolved reference 'EglBase'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:166:83 Unresolved reference 'CameraVideoCapturer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:168:28 Unresolved reference 'initialize'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:168:83 Unresolved reference 'capturerObserver'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:169:28 Unresolved reference 'startCapture'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:174:27 Unresolved reference 'MediaConstraints'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:175:25 Unresolved reference 'createOffer'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:175:46 Unresolved reference 'SdpObserver'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:176:13 'onCreateSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:176:48 Unresolved reference 'SessionDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:177:33 Unresolved reference 'setLocalDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:177:62 Unresolved reference 'SdpObserver'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:178:21 'onSetSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:182:45 Unresolved reference 'description'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:186:21 'onSetFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:187:21 'onCreateSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:187:54 Unresolved reference 'SessionDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:188:21 'onCreateFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:191:13 'onCreateFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:192:13 'onSetSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:193:13 'onSetFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:198:20 Unresolved reference 'SessionDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:198:39 Unresolved reference 'SessionDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:199:25 Unresolved reference 'setRemoteDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:199:55 Unresolved reference 'SdpObserver'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:200:13 'onSetSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:203:13 'onSetFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:204:13 'onCreateSuccess' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:204:46 Unresolved reference 'SessionDescription'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:205:13 'onCreateFailure' overrides nothing.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:210:28 Unresolved reference 'IceCandidate'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:211:25 Unresolved reference 'addIceCandidate'.
|
||||
e: file:///home/trevor/AndroidStudioProjects/GodEye/app/src/main/java/com/example/godeye/streaming.disabled/WebRTCStreamManager.kt:216:32 Unresolved reference 'dispose'.
|
||||
|
||||
FAILURE: Build failed with an exception.
|
||||
|
||||
* What went wrong:
|
||||
Execution failed for task ':app:compileDebugKotlin'.
|
||||
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
|
||||
> Compilation error. See log for more details
|
||||
|
||||
* Try:
|
||||
> Run with --stacktrace option to get the stack trace.
|
||||
> Run with --info or --debug option to get more log output.
|
||||
> Run with --scan to get full insights.
|
||||
> Get more help at https://help.gradle.org.
|
||||
|
||||
BUILD FAILED in 2s
|
||||
31 actionable tasks: 13 executed, 18 from cache
|
||||
6
build_output.log
Normal file
6
build_output.log
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
ERROR: JAVA_HOME is set to an invalid directory: /usr/lib/jvm/java-11-openjdk-amd64
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation.
|
||||
|
||||
350
godeye_server.py
Normal file
350
godeye_server.py
Normal file
@@ -0,0 +1,350 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Простой веб-сервер для панели оператора GodEye
|
||||
Обслуживает WebRTC интерфейс и Socket.IO соединения
|
||||
"""
|
||||
|
||||
import socketio
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
from flask import Flask, send_file
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
# Создаем Flask приложение
|
||||
app = Flask(__name__)
|
||||
|
||||
# Создаем Socket.IO сервер
|
||||
sio = socketio.Server(cors_allowed_origins="*")
|
||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||
|
||||
# Хранилище подключенных устройств и операторов
|
||||
connected_devices = {}
|
||||
connected_operators = {}
|
||||
active_sessions = {}
|
||||
|
||||
@app.route('/')
|
||||
def serve_interface():
|
||||
"""Обслуживает панель оператора"""
|
||||
return send_file('operator-interface.html')
|
||||
|
||||
@sio.event
|
||||
def connect(sid, environ):
|
||||
"""Обработка подключения клиента"""
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔗 Клиент подключился: {sid}")
|
||||
|
||||
@sio.event
|
||||
def disconnect(sid):
|
||||
"""Обработка отключения клиента"""
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ Клиент отключился: {sid}")
|
||||
|
||||
# Удаляем из устройств
|
||||
if sid in connected_devices:
|
||||
device_data = connected_devices[sid]
|
||||
del connected_devices[sid]
|
||||
sio.emit('device_disconnected', {
|
||||
'deviceId': device_data['deviceId'],
|
||||
'deviceName': device_data['deviceName']
|
||||
})
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📱 Устройство отключилось: {device_data['deviceName']}")
|
||||
|
||||
# Удаляем из операторов
|
||||
if sid in connected_operators:
|
||||
del connected_operators[sid]
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 👨💼 Оператор отключился")
|
||||
|
||||
@sio.event
|
||||
def device_register(sid, data):
|
||||
"""Регистрация Android устройства"""
|
||||
device_id = data.get('deviceId')
|
||||
device_name = data.get('deviceName', 'Unknown Device')
|
||||
|
||||
connected_devices[sid] = {
|
||||
'deviceId': device_id,
|
||||
'deviceName': device_name,
|
||||
'connectedAt': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📱 Устройство зарегистрировано: {device_name} ({device_id})")
|
||||
|
||||
# Уведомляем всех операторов о новом устройстве
|
||||
sio.emit('device_connected', {
|
||||
'deviceId': device_id,
|
||||
'deviceName': device_name
|
||||
})
|
||||
|
||||
# Отправляем подтверждение устройству
|
||||
sio.emit('registration_confirmed', {
|
||||
'status': 'success',
|
||||
'message': 'Устройство успешно зарегистрировано'
|
||||
}, room=sid)
|
||||
|
||||
@sio.event
|
||||
def operator_register(sid, data):
|
||||
"""Регистрация оператора"""
|
||||
operator_id = data.get('operatorId', f'operator_{uuid.uuid4().hex[:8]}')
|
||||
|
||||
connected_operators[sid] = {
|
||||
'operatorId': operator_id,
|
||||
'connectedAt': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 👨💼 Оператор зарегистрирован: {operator_id}")
|
||||
|
||||
# Отправляем список подключенных устройств
|
||||
devices_list = []
|
||||
for device_sid, device_data in connected_devices.items():
|
||||
devices_list.append({
|
||||
'deviceId': device_data['deviceId'],
|
||||
'deviceName': device_data['deviceName']
|
||||
})
|
||||
|
||||
sio.emit('devices_list', {'devices': devices_list}, room=sid)
|
||||
|
||||
@sio.event
|
||||
def request_camera(sid, data):
|
||||
"""Запрос камеры от оператора"""
|
||||
device_id = data.get('deviceId')
|
||||
operator_id = data.get('operatorId')
|
||||
camera_type = data.get('cameraType', 'back') # Убеждаемся что есть значение по умолчанию
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📹 Запрос камеры от {operator_id} для устройства {device_id}, тип камеры: {camera_type}")
|
||||
|
||||
# Находим устройство по ID
|
||||
target_device_sid = None
|
||||
for device_sid, device_data in connected_devices.items():
|
||||
if device_data['deviceId'] == device_id:
|
||||
target_device_sid = device_sid
|
||||
break
|
||||
|
||||
if target_device_sid:
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
# Сохраняем сессию с правильными данными
|
||||
active_sessions[session_id] = {
|
||||
'deviceSid': target_device_sid,
|
||||
'operatorSid': sid,
|
||||
'deviceId': device_id,
|
||||
'operatorId': operator_id,
|
||||
'cameraType': camera_type, # Явно сохраняем тип камеры
|
||||
'status': 'pending', # Добавляем статус
|
||||
'startedAt': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Отправляем запрос устройству с полными данными
|
||||
sio.emit('camera_request', {
|
||||
'sessionId': session_id,
|
||||
'operatorId': operator_id,
|
||||
'cameraType': camera_type,
|
||||
'deviceId': device_id, # Добавляем deviceId
|
||||
'message': f'Оператор {operator_id} запрашивает доступ к камере'
|
||||
}, room=target_device_sid)
|
||||
|
||||
# Уведомляем оператора о создании сессии
|
||||
sio.emit('session_created', {
|
||||
'sessionId': session_id,
|
||||
'deviceId': device_id,
|
||||
'cameraType': camera_type,
|
||||
'status': 'pending'
|
||||
}, room=sid)
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📤 Запрос отправлен устройству, сессия: {session_id}, камера: {camera_type}")
|
||||
else:
|
||||
sio.emit('error', {
|
||||
'message': f'Устройство {device_id} не найдено'
|
||||
}, room=sid)
|
||||
|
||||
@sio.event
|
||||
def camera_approved(sid, data):
|
||||
"""Подтверждение доступа к камере от устройства"""
|
||||
session_id = data.get('sessionId')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
operator_sid = session['operatorSid']
|
||||
|
||||
# Обновляем статус сессии
|
||||
active_sessions[session_id]['status'] = 'approved'
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] ✅ Доступ к камере подтвержден для сессии: {session_id}")
|
||||
|
||||
# Уведомляем оператора с полными данными
|
||||
sio.emit('camera_approved', {
|
||||
'sessionId': session_id,
|
||||
'deviceId': session['deviceId'],
|
||||
'cameraType': session['cameraType'],
|
||||
'status': 'approved'
|
||||
}, room=operator_sid)
|
||||
|
||||
@sio.event
|
||||
def camera_rejected(sid, data):
|
||||
"""Отклонение доступа к камере от устройства"""
|
||||
session_id = data.get('sessionId')
|
||||
reason = data.get('reason', 'Отклонено пользователем')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
operator_sid = session['operatorSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] ❌ Доступ к камере отклонен для сессии: {session_id}, причина: {reason}")
|
||||
|
||||
# Уведомляем оператора об отклонении
|
||||
sio.emit('camera_rejected', {
|
||||
'sessionId': session_id,
|
||||
'deviceId': session['deviceId'],
|
||||
'reason': reason
|
||||
}, room=operator_sid)
|
||||
|
||||
# Удаляем сессию, так как она отклонена
|
||||
del active_sessions[session_id]
|
||||
|
||||
@sio.event
|
||||
def camera_started(sid, data):
|
||||
"""Уведомление о запуске камеры"""
|
||||
session_id = data.get('sessionId')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
operator_sid = session['operatorSid']
|
||||
|
||||
# Обновляем статус сессии на активный
|
||||
active_sessions[session_id]['status'] = 'active'
|
||||
active_sessions[session_id]['actualStartedAt'] = datetime.now().isoformat()
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📹 Камера запущена для сессии: {session_id}")
|
||||
|
||||
# Уведомляем оператора с обновленным статусом
|
||||
sio.emit('camera_started', {
|
||||
'sessionId': session_id,
|
||||
'deviceId': session['deviceId'],
|
||||
'cameraType': session['cameraType'],
|
||||
'status': 'active'
|
||||
}, room=operator_sid)
|
||||
|
||||
# Также отправляем обновление статуса сессии
|
||||
sio.emit('session_status_update', {
|
||||
'sessionId': session_id,
|
||||
'status': 'active',
|
||||
'cameraType': session['cameraType']
|
||||
}, room=operator_sid)
|
||||
|
||||
@sio.event
|
||||
def webrtc_offer(sid, data):
|
||||
"""Пересылка WebRTC offer от устройства к оператору"""
|
||||
session_id = data.get('sessionId')
|
||||
offer = data.get('offer')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
operator_sid = session['operatorSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 Пересылка WebRTC offer для сессии: {session_id}")
|
||||
|
||||
# Пересылаем offer оператору
|
||||
sio.emit('webrtc_offer', {
|
||||
'sessionId': session_id,
|
||||
'offer': offer
|
||||
}, room=operator_sid)
|
||||
|
||||
@sio.event
|
||||
def webrtc_answer(sid, data):
|
||||
"""Пересылка WebRTC answer от оператора к устройству"""
|
||||
session_id = data.get('sessionId')
|
||||
answer = data.get('answer')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
device_sid = session['deviceSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 📡 Пересылка WebRTC answer для сессии: {session_id}")
|
||||
|
||||
# Пересылаем answer устройству
|
||||
sio.emit('webrtc_answer', {
|
||||
'sessionId': session_id,
|
||||
'answer': answer
|
||||
}, room=device_sid)
|
||||
|
||||
@sio.event
|
||||
def webrtc_ice_candidate(sid, data):
|
||||
"""Пересылка ICE candidates между устройством и оператором"""
|
||||
session_id = data.get('sessionId')
|
||||
candidate = data.get('candidate')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
|
||||
# Определяем куда пересылать (от устройства к оператору или наоборот)
|
||||
if sid == session['deviceSid']:
|
||||
# От устройства к оператору
|
||||
target_sid = session['operatorSid']
|
||||
else:
|
||||
# От оператора к устройству
|
||||
target_sid = session['deviceSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🧊 Пересылка ICE candidate для сессии: {session_id}")
|
||||
|
||||
sio.emit('webrtc_ice_candidate', {
|
||||
'sessionId': session_id,
|
||||
'candidate': candidate
|
||||
}, room=target_sid)
|
||||
|
||||
@sio.event
|
||||
def switch_camera(sid, data):
|
||||
"""Переключение камеры"""
|
||||
session_id = data.get('sessionId')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
device_sid = session['deviceSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🔄 Переключение камеры для сессии: {session_id}")
|
||||
|
||||
# Отправляем команду устройству
|
||||
sio.emit('switch_camera', {
|
||||
'sessionId': session_id
|
||||
}, room=device_sid)
|
||||
|
||||
@sio.event
|
||||
def stop_camera(sid, data):
|
||||
"""Остановка камеры"""
|
||||
session_id = data.get('sessionId')
|
||||
|
||||
if session_id in active_sessions:
|
||||
session = active_sessions[session_id]
|
||||
device_sid = session['deviceSid']
|
||||
operator_sid = session['operatorSid']
|
||||
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] 🛑 Остановка камеры для сессии: {session_id}")
|
||||
|
||||
# Отправляем команду устройству
|
||||
sio.emit('stop_camera', {
|
||||
'sessionId': session_id
|
||||
}, room=device_sid)
|
||||
|
||||
# Уведомляем оператора
|
||||
sio.emit('camera_stopped', {
|
||||
'sessionId': session_id
|
||||
}, room=operator_sid)
|
||||
|
||||
# Удаляем сессию
|
||||
del active_sessions[session_id]
|
||||
|
||||
if __name__ == '__main__':
|
||||
# ИСПРАВЛЕНО: Используем localhost для локального тестирования
|
||||
host = '0.0.0.0' # Слушаем на всех интерфейсах
|
||||
port = 3001
|
||||
|
||||
print("🚀 Запуск GodEye сервера...")
|
||||
print(f"🌐 Веб-интерфейс: http://localhost:{port}")
|
||||
print(f"🔌 Socket.IO endpoint: http://localhost:{port}/socket.io/")
|
||||
print("📱 Настройте Android приложение на адрес вашего компьютера")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
eventlet.wsgi.server(eventlet.listen((host, port)), app)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Сервер остановлен")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка сервера: {e}")
|
||||
@@ -7,6 +7,10 @@
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
|
||||
# ?????????? Java 17 ??? ??????????? ??????????
|
||||
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
@@ -20,4 +24,7 @@ kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
# Enable Compose Compiler
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
[versions]
|
||||
agp = "8.13.0"
|
||||
kotlin = "2.0.21"
|
||||
kotlin = "2.0.20"
|
||||
coreKtx = "1.17.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.9.4"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.11.0"
|
||||
composeBom = "2024.09.00"
|
||||
composeBom = "2025.09.01"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.12.0"
|
||||
constraintlayout = "2.2.0"
|
||||
preference = "1.2.1"
|
||||
coroutines = "1.9.0"
|
||||
okhttp = "4.12.0"
|
||||
socketio = "2.1.1"
|
||||
gson = "2.10.1"
|
||||
webrtc = "1.0.32006"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -17,16 +26,34 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
androidx-preference = { group = "androidx.preference", name = "preference-ktx", version.ref = "preference" }
|
||||
|
||||
# Coroutines
|
||||
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
|
||||
# Network
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
socketio-client = { group = "io.socket", name = "socket.io-client", version.ref = "socketio" }
|
||||
|
||||
# JSON
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
|
||||
# WebRTC - используем Stream WebRTC библиотеку которая доступна
|
||||
webrtc-android = { group = "io.getstream", name = "stream-webrtc-android", version = "1.0.7" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
|
||||
@@ -3,11 +3,28 @@
|
||||
# Автоматическая сборка, установка APK на LG G6 и мониторинг логов
|
||||
# Использование: ./install_to_lg_g6.sh
|
||||
|
||||
# Принудительно используем Java 17 - ту же версию что в gradle.properties
|
||||
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
|
||||
export GRADLE_OPTS="-Dorg.gradle.java.home=/usr/lib/jvm/java-17-openjdk-amd64 -Dorg.gradle.jvmargs=-Xmx2048m"
|
||||
export PATH="/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH"
|
||||
|
||||
# Проверяем что Java 17 доступна
|
||||
if [ ! -d "$JAVA_HOME" ]; then
|
||||
echo "❌ Java 17 не найдена в $JAVA_HOME"
|
||||
echo "Проверим доступные версии Java:"
|
||||
ls -la /usr/lib/jvm/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔧 GodEye APK Builder & Installer для LG G6"
|
||||
echo "=============================================="
|
||||
echo "🔧 JAVA_HOME: $JAVA_HOME"
|
||||
echo "🔧 Java версия: $($JAVA_HOME/bin/java -version 2>&1 | head -1)"
|
||||
|
||||
APK_PATH="/home/trevor/AndroidStudioProjects/GodEye/app/build/outputs/apk/debug/app-debug.apk"
|
||||
LG_G6_DEVICE="LGMG600S9b4da66b"
|
||||
APP_PACKAGE="com.example.godeye"
|
||||
MAIN_ACTIVITY=".LegacyMainActivity"
|
||||
|
||||
# Функция для логирования с временными метками
|
||||
log() {
|
||||
@@ -33,7 +50,7 @@ log "✅ LG G6 найден: $LG_G6_DEVICE"
|
||||
|
||||
# Собираем проект
|
||||
log "🔨 Собираю проект..."
|
||||
if ! ./gradlew assembleDebug; then
|
||||
if ! JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 ./gradlew assembleDebug --no-daemon; then
|
||||
echo "❌ Ошибка сборки проекта!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -61,7 +78,7 @@ adb -s "$LG_G6_DEVICE" logcat -c
|
||||
|
||||
# Запускаем приложение
|
||||
log "🚀 Запускаю приложение GodEye..."
|
||||
adb -s "$LG_G6_DEVICE" shell am start -n com.example.godeye/.MainActivity
|
||||
adb -s "$LG_G6_DEVICE" shell am start -n ${APP_PACKAGE}/${MAIN_ACTIVITY}
|
||||
|
||||
# Ждем немного для инициализации
|
||||
sleep 2
|
||||
|
||||
4
logs/signaling.log
Normal file
4
logs/signaling.log
Normal file
@@ -0,0 +1,4 @@
|
||||
Traceback (most recent call last):
|
||||
File "/home/trevor/AndroidStudioProjects/GodEye/signaling_server.py", line 8, in <module>
|
||||
import websockets
|
||||
ModuleNotFoundError: No module named 'websockets'
|
||||
219
monitor_network.sh
Executable file
219
monitor_network.sh
Executable file
@@ -0,0 +1,219 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GodEye Network Traffic Monitor
|
||||
# Скрипт для отслеживания сетевого трафика Android приложения
|
||||
|
||||
echo "🔍 GodEye Network Traffic Monitor"
|
||||
echo "=================================="
|
||||
|
||||
# Исправленная проверка подключения устройства
|
||||
# Проверяем все подключенные устройства
|
||||
echo "📱 Поиск устройств Android..."
|
||||
DEVICES=($(adb devices | grep -w "device" | awk '{print $1}'))
|
||||
|
||||
if [ ${#DEVICES[@]} -eq 0 ]; then
|
||||
echo "❌ Устройства Android не найдены!"
|
||||
echo "Убедитесь что:"
|
||||
echo "1. Устройство подключено по USB"
|
||||
echo "2. Включена отладка по USB"
|
||||
echo "3. Разрешена отладка для этого компьютера"
|
||||
exit 1
|
||||
elif [ ${#DEVICES[@]} -eq 1 ]; then
|
||||
DEVICE=${DEVICES[0]}
|
||||
echo "📱 Найдено устройство: $DEVICE"
|
||||
else
|
||||
echo "📱 Найдено несколько устройств:"
|
||||
for i in "${!DEVICES[@]}"; do
|
||||
echo "$((i+1))) ${DEVICES[$i]}"
|
||||
done
|
||||
|
||||
# Проверяем есть ли LG G6
|
||||
LG_G6="LGMG600S9b4da66b"
|
||||
if [[ " ${DEVICES[@]} " =~ " ${LG_G6} " ]]; then
|
||||
DEVICE=$LG_G6
|
||||
echo "🎯 Автоматически выбран LG G6: $DEVICE"
|
||||
else
|
||||
read -p "Выберите устройство (1-${#DEVICES[@]}): " choice
|
||||
if [[ $choice -ge 1 && $choice -le ${#DEVICES[@]} ]]; then
|
||||
DEVICE=${DEVICES[$((choice-1))]}
|
||||
echo "✅ Выбрано устройство: $DEVICE"
|
||||
else
|
||||
echo "❌ Неверный выбор!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Функция для выполнения adb команд с указанием конкретного устройства
|
||||
adb_cmd() {
|
||||
adb -s "$DEVICE" "$@"
|
||||
}
|
||||
|
||||
# Функция для отслеживания логов приложения
|
||||
monitor_app_logs() {
|
||||
echo ""
|
||||
echo "📋 Мониторинг логов приложения GodEye..."
|
||||
echo "Фильтр: WebRTC, Network, Connection"
|
||||
echo "Нажмите Ctrl+C для остановки"
|
||||
echo "----------------------------------------"
|
||||
|
||||
adb_cmd logcat -c # Очищаем логи
|
||||
adb_cmd logcat | grep -E "(WEBRTC|WebRTC|GodEye|Network|Connection)" --color=always
|
||||
}
|
||||
|
||||
# Функция для отслеживания сетевых соединений
|
||||
monitor_network_connections() {
|
||||
echo ""
|
||||
echo "🌐 Мониторинг сетевых соединений..."
|
||||
echo "Поиск активных UDP/TCP соединений от GodEye"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Получаем PID приложения GodEye
|
||||
APP_PID=$(adb_cmd shell ps | grep com.example.godeye | awk '{print $2}' | head -1)
|
||||
|
||||
if [ -z "$APP_PID" ]; then
|
||||
echo "❌ Приложение GodEye не запущено!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "🎯 PID приложения GodEye: $APP_PID"
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
echo "$(date '+%H:%M:%S') - Активные соединения:"
|
||||
adb_cmd shell netstat | grep $APP_PID | while read line; do
|
||||
echo " 📡 $line"
|
||||
done
|
||||
echo ""
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
# Функция для анализа трафика через tcpdump (требует root)
|
||||
monitor_packet_capture() {
|
||||
echo ""
|
||||
echo "📦 Захват пакетов (требует root)..."
|
||||
echo "Попытка захвата UDP трафика на порты WebRTC"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Проверяем root доступ
|
||||
ROOT_CHECK=$(adb_cmd shell su -c "echo test" 2>/dev/null)
|
||||
if [ "$ROOT_CHECK" != "test" ]; then
|
||||
echo "❌ Root доступ недоступен. Пропускаем захват пакетов."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ Root доступ получен"
|
||||
echo "Захватываем UDP трафик (порты 1024-65535)..."
|
||||
|
||||
# Захватываем UDP пакеты
|
||||
adb_cmd shell su -c "tcpdump -i any -n udp and portrange 1024-65535" | \
|
||||
while read line; do
|
||||
# Фильтруем только интересные пакеты
|
||||
if echo "$line" | grep -E "(STUN|RTP|DTLS)" > /dev/null; then
|
||||
echo "🚀 WebRTC: $line"
|
||||
elif echo "$line" | grep -E "([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" > /dev/null; then
|
||||
echo "📡 UDP: $line"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Функция для мониторинга использования сети приложением
|
||||
monitor_data_usage() {
|
||||
echo ""
|
||||
echo "📊 Мониторинг использования данных..."
|
||||
echo "Отслеживание трафика приложения GodEye"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Получаем UID приложения (исправленная переменная)
|
||||
local APP_UID=$(adb_cmd shell dumpsys package com.example.godeye | grep "userId=" | head -1 | sed 's/.*userId=\([0-9]*\).*/\1/')
|
||||
|
||||
if [ -z "$APP_UID" ]; then
|
||||
echo "❌ Не удалось получить UID приложения!"
|
||||
echo "Проверяем установленные пакеты..."
|
||||
adb_cmd shell pm list packages | grep godeye
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "🆔 UID приложения: $APP_UID"
|
||||
echo ""
|
||||
|
||||
# Мониторим использование сети
|
||||
while true; do
|
||||
echo "$(date '+%H:%M:%S') - Статистика сети:"
|
||||
|
||||
# Получаем статистику из /proc/net/xt_qtaguid/stats
|
||||
STATS=$(adb_cmd shell cat /proc/net/xt_qtaguid/stats 2>/dev/null | grep " $APP_UID ")
|
||||
|
||||
if [ ! -z "$STATS" ]; then
|
||||
echo "$STATS" | while read line; do
|
||||
# Парсим статистику
|
||||
RX_BYTES=$(echo $line | awk '{print $6}')
|
||||
TX_BYTES=$(echo $line | awk '{print $8}')
|
||||
|
||||
if [ "$RX_BYTES" -gt 0 ] || [ "$TX_BYTES" -gt 0 ]; then
|
||||
echo " 📥 Получено: $(format_bytes $RX_BYTES)"
|
||||
echo " 📤 Отправлено: $(format_bytes $TX_BYTES)"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " ℹ️ Сетевая активность не обнаружена"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
# Функция форматирования байтов
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
if [ $bytes -lt 1024 ]; then
|
||||
echo "${bytes} B"
|
||||
elif [ $bytes -lt $((1024*1024)) ]; then
|
||||
echo "$((bytes/1024)) KB"
|
||||
elif [ $bytes -lt $((1024*1024*1024)) ]; then
|
||||
echo "$((bytes/1024/1024)) MB"
|
||||
else
|
||||
echo "$((bytes/1024/1024/1024)) GB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Главное меню
|
||||
echo ""
|
||||
echo "Выберите режим мониторинга:"
|
||||
echo "1) 📋 Логи приложения (WebRTC/Network)"
|
||||
echo "2) 🌐 Сетевые соединения"
|
||||
echo "3) 📦 Захват пакетов (root)"
|
||||
echo "4) 📊 Использование данных"
|
||||
echo "5) 🔄 Все режимы одновременно"
|
||||
echo ""
|
||||
|
||||
read -p "Введите номер (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
monitor_app_logs
|
||||
;;
|
||||
2)
|
||||
monitor_network_connections
|
||||
;;
|
||||
3)
|
||||
monitor_packet_capture
|
||||
;;
|
||||
4)
|
||||
monitor_data_usage
|
||||
;;
|
||||
5)
|
||||
echo "🚀 Запуск всех мониторов..."
|
||||
echo "Откройте дополнительные терминалы для других режимов"
|
||||
monitor_app_logs &
|
||||
monitor_network_connections &
|
||||
monitor_data_usage &
|
||||
wait
|
||||
;;
|
||||
*)
|
||||
echo "❌ Неверный выбор!"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
533
operator-interface.html
Normal file
533
operator-interface.html
Normal file
@@ -0,0 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GodEye - Интерфейс Оператора</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status.disconnected { background: #d32f2f; }
|
||||
.status.connecting { background: #f57c00; }
|
||||
.status.connected { background: #388e3c; }
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#remoteVideo {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary { background: #2196f3; color: white; }
|
||||
.btn-success { background: #4caf50; color: white; }
|
||||
.btn-danger { background: #f44336; color: white; }
|
||||
.btn-warning { background: #ff9800; color: white; }
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.device-list {
|
||||
background: #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
background: #3a3a3a;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.device-item:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.logs {
|
||||
background: #0a0a0a;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin: 2px 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.log-info { color: #81c784; }
|
||||
.log-warning { color: #ffb74d; }
|
||||
.log-error { color: #e57373; }
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.network-monitor {
|
||||
background: #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.traffic-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.traffic-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.traffic-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.upload { background: #4caf50; }
|
||||
.download { background: #2196f3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🕵️ GodEye - Интерфейс Оператора</h1>
|
||||
<div id="connectionStatus" class="status disconnected">
|
||||
🔴 Не подключен к серверу сигналинга
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="connectBtn" class="btn-primary" onclick="connectToSignaling()">
|
||||
📡 Подключиться к серверу
|
||||
</button>
|
||||
<button id="refreshBtn" class="btn-warning" onclick="refreshDevices()">
|
||||
🔄 Обновить устройства
|
||||
</button>
|
||||
<button id="disconnectBtn" class="btn-danger" onclick="disconnect()" disabled>
|
||||
🔌 Отключиться
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="device-list">
|
||||
<h3>📱 Доступные устройства</h3>
|
||||
<div id="deviceList">
|
||||
<p>Нет доступных устройств. Убедитесь, что приложение запущено на устройстве.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-container">
|
||||
<video id="remoteVideo" autoplay playsinline muted>
|
||||
<p>Видео поток будет отображен здесь после подключения к устройству</p>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="connectionTime">--:--</div>
|
||||
<div>Время подключения</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="videoQuality">---</div>
|
||||
<div>Качество видео</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="latency">--- ms</div>
|
||||
<div>Задержка</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="bandwidth">--- kbps</div>
|
||||
<div>Пропускная способность</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="network-monitor">
|
||||
<h3>📊 Мониторинг сети</h3>
|
||||
<div class="traffic-indicator">
|
||||
<span>📤 Исходящий:</span>
|
||||
<div class="traffic-bar">
|
||||
<div id="uploadBar" class="traffic-fill upload" style="width: 0%"></div>
|
||||
</div>
|
||||
<span id="uploadSpeed">0 KB/s</span>
|
||||
</div>
|
||||
<div class="traffic-indicator">
|
||||
<span>📥 Входящий:</span>
|
||||
<div class="traffic-bar">
|
||||
<div id="downloadBar" class="traffic-fill download" style="width: 0%"></div>
|
||||
</div>
|
||||
<span id="downloadSpeed">0 KB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs">
|
||||
<div id="logContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Глобальные переменные
|
||||
let ws = null;
|
||||
let pc = null;
|
||||
let currentSession = null;
|
||||
let connectionStartTime = null;
|
||||
let statsInterval = null;
|
||||
|
||||
// WebRTC конфигурация
|
||||
const pcConfig = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||||
]
|
||||
};
|
||||
|
||||
// Функции логирования
|
||||
function log(message, type = 'info') {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${type}`;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
// Подключение к серверу сигналинга
|
||||
function connectToSignaling() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
log('Уже подключен к серверу сигналинга', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Подключение к серверу сигналинга...', 'info');
|
||||
updateConnectionStatus('connecting', '🟡 Подключение к серверу...');
|
||||
|
||||
ws = new WebSocket('ws://localhost:8765');
|
||||
|
||||
ws.onopen = function() {
|
||||
log('✅ Подключен к серверу сигналинга', 'info');
|
||||
updateConnectionStatus('connected', '🟢 Подключен к серверу сигналинга');
|
||||
document.getElementById('connectBtn').disabled = true;
|
||||
document.getElementById('disconnectBtn').disabled = false;
|
||||
refreshDevices();
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
handleSignalingMessage(data);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
log('❌ Соединение с сервером разорвано', 'error');
|
||||
updateConnectionStatus('disconnected', '🔴 Не подключен к серверу');
|
||||
document.getElementById('connectBtn').disabled = false;
|
||||
document.getElementById('disconnectBtn').disabled = true;
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
log('❌ Ошибка подключения к серверу: ' + error, 'error');
|
||||
updateConnectionStatus('disconnected', '🔴 Ошибка подключения');
|
||||
};
|
||||
}
|
||||
|
||||
// Обработка сообщений сигналинга
|
||||
function handleSignalingMessage(data) {
|
||||
log(`📨 Получено сообщение: ${data.type}`, 'info');
|
||||
|
||||
switch(data.type) {
|
||||
case 'client_registered':
|
||||
log(`🆔 Зарегистрирован как клиент: ${data.client_id}`, 'info');
|
||||
break;
|
||||
|
||||
case 'session_joined':
|
||||
currentSession = data.session_id;
|
||||
log(`✅ Подключен к сессии: ${data.session_id}`, 'info');
|
||||
log(`📱 Устройство: ${data.device_info.model || 'Unknown'}`, 'info');
|
||||
connectionStartTime = Date.now();
|
||||
startStatsMonitoring();
|
||||
break;
|
||||
|
||||
case 'offer':
|
||||
handleOffer(data.sdp);
|
||||
break;
|
||||
|
||||
case 'ice_candidate':
|
||||
handleIceCandidate(data.candidate);
|
||||
break;
|
||||
|
||||
case 'hangup':
|
||||
handleHangup();
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
log(`❌ Ошибка: ${data.message}`, 'error');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка SDP offer
|
||||
async function handleOffer(offer) {
|
||||
try {
|
||||
log('📞 Получен SDP Offer, создание PeerConnection...', 'info');
|
||||
|
||||
pc = new RTCPeerConnection(pcConfig);
|
||||
|
||||
pc.onicecandidate = function(event) {
|
||||
if (event.candidate) {
|
||||
log('🧊 Отправка ICE candidate', 'info');
|
||||
sendMessage({
|
||||
type: 'ice_candidate',
|
||||
session_id: currentSession,
|
||||
candidate: event.candidate
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
pc.ontrack = function(event) {
|
||||
log('📹 Получен видео поток', 'info');
|
||||
const remoteVideo = document.getElementById('remoteVideo');
|
||||
remoteVideo.srcObject = event.streams[0];
|
||||
};
|
||||
|
||||
await pc.setRemoteDescription(offer);
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
|
||||
log('📞 Отправка SDP Answer', 'info');
|
||||
sendMessage({
|
||||
type: 'answer',
|
||||
session_id: currentSession,
|
||||
sdp: answer
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка обработки offer: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка ICE candidate
|
||||
async function handleIceCandidate(candidate) {
|
||||
try {
|
||||
if (pc) {
|
||||
await pc.addIceCandidate(candidate);
|
||||
log('🧊 ICE candidate добавлен', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Ошибка добавления ICE candidate: ${error}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Подключение к устройству
|
||||
function connectToDevice(sessionId) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
log('❌ Нет подключения к серверу сигналинга', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`🔗 Подключение к устройству в сессии: ${sessionId}`, 'info');
|
||||
|
||||
sendMessage({
|
||||
type: 'join_session',
|
||||
session_id: sessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Отправка сообщения через WebSocket
|
||||
function sendMessage(message) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление статуса подключения
|
||||
function updateConnectionStatus(status, text) {
|
||||
const statusElement = document.getElementById('connectionStatus');
|
||||
statusElement.className = `status ${status}`;
|
||||
statusElement.textContent = text;
|
||||
}
|
||||
|
||||
// Обновление списка устройств
|
||||
function refreshDevices() {
|
||||
const deviceList = document.getElementById('deviceList');
|
||||
deviceList.innerHTML = '<p>🔍 Поиск доступных устройств...</p>';
|
||||
|
||||
// Имитация поиска устройств (в реальности будет запрос к серверу)
|
||||
setTimeout(() => {
|
||||
deviceList.innerHTML = `
|
||||
<div class="device-item" onclick="connectToDevice('demo-session-1')">
|
||||
<strong>📱 LG G6 (LGMG600S9b4da66b)</strong><br>
|
||||
<small>Android 8.0 • IP: 192.168.1.100 • Последняя активность: только что</small>
|
||||
</div>
|
||||
`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Завершение соединения
|
||||
function disconnect() {
|
||||
if (pc) {
|
||||
pc.close();
|
||||
pc = null;
|
||||
}
|
||||
|
||||
if (currentSession) {
|
||||
sendMessage({
|
||||
type: 'hangup',
|
||||
session_id: currentSession
|
||||
});
|
||||
currentSession = null;
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
ws.close();
|
||||
ws = null;
|
||||
}
|
||||
|
||||
if (statsInterval) {
|
||||
clearInterval(statsInterval);
|
||||
statsInterval = null;
|
||||
}
|
||||
|
||||
connectionStartTime = null;
|
||||
updateConnectionStatus('disconnected', '🔴 Отключен');
|
||||
document.getElementById('remoteVideo').srcObject = null;
|
||||
|
||||
log('🔌 Соединение разорвано', 'info');
|
||||
}
|
||||
|
||||
// Обработка завершения сессии
|
||||
function handleHangup() {
|
||||
log('📴 Сессия завершена', 'info');
|
||||
disconnect();
|
||||
}
|
||||
|
||||
// Мониторинг статистики
|
||||
function startStatsMonitoring() {
|
||||
if (statsInterval) {
|
||||
clearInterval(statsInterval);
|
||||
}
|
||||
|
||||
statsInterval = setInterval(updateStats, 1000);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
// Обновление времени подключения
|
||||
if (connectionStartTime) {
|
||||
const elapsed = Math.floor((Date.now() - connectionStartTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
document.getElementById('connectionTime').textContent =
|
||||
`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// WebRTC статистика (упрощенная)
|
||||
if (pc) {
|
||||
pc.getStats().then(stats => {
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||||
const fps = report.framesPerSecond || 0;
|
||||
const bitrate = Math.round((report.bytesReceived || 0) * 8 / 1000);
|
||||
|
||||
document.getElementById('videoQuality').textContent = `${fps} FPS`;
|
||||
document.getElementById('bandwidth').textContent = `${bitrate} kbps`;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Автоподключение при загрузке страницы
|
||||
window.onload = function() {
|
||||
log('🚀 GodEye Operator Interface загружен', 'info');
|
||||
|
||||
// Попытка автоподключения через 1 секунду
|
||||
setTimeout(() => {
|
||||
log('🔄 Попытка автоподключения к серверу...', 'info');
|
||||
connectToSignaling();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Обработка закрытия окна
|
||||
window.onbeforeunload = function() {
|
||||
disconnect();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
269
signaling_server.py
Normal file
269
signaling_server.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GodEye Signaling Server
|
||||
Сервер для обмена SDP и ICE кандидатами между устройством и оператором
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SignalingServer:
|
||||
def __init__(self):
|
||||
self.clients = {} # client_id -> websocket
|
||||
self.sessions = {} # session_id -> {device: ws, operator: ws}
|
||||
|
||||
async def register_client(self, websocket, path):
|
||||
"""Регистрация нового клиента"""
|
||||
client_id = str(uuid.uuid4())
|
||||
self.clients[client_id] = websocket
|
||||
|
||||
logger.info(f"🔗 Клиент подключен: {client_id}")
|
||||
|
||||
try:
|
||||
# Отправляем ID клиенту
|
||||
await websocket.send(json.dumps({
|
||||
"type": "client_registered",
|
||||
"client_id": client_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
async for message in websocket:
|
||||
await self.handle_message(client_id, message)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.info(f"🔌 Клиент отключен: {client_id}")
|
||||
finally:
|
||||
# Очистка при отключении
|
||||
await self.cleanup_client(client_id)
|
||||
|
||||
async def handle_message(self, client_id, message):
|
||||
"""Обработка сообщений от клиентов"""
|
||||
try:
|
||||
data = json.loads(message)
|
||||
message_type = data.get("type")
|
||||
|
||||
logger.info(f"📨 Сообщение от {client_id}: {message_type}")
|
||||
|
||||
if message_type == "create_session":
|
||||
await self.create_session(client_id, data)
|
||||
elif message_type == "join_session":
|
||||
await self.join_session(client_id, data)
|
||||
elif message_type == "offer":
|
||||
await self.relay_offer(client_id, data)
|
||||
elif message_type == "answer":
|
||||
await self.relay_answer(client_id, data)
|
||||
elif message_type == "ice_candidate":
|
||||
await self.relay_ice_candidate(client_id, data)
|
||||
elif message_type == "hangup":
|
||||
await self.handle_hangup(client_id, data)
|
||||
else:
|
||||
logger.warning(f"⚠️ Неизвестный тип сообщения: {message_type}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"❌ Ошибка парсинга JSON от {client_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обработки сообщения: {e}")
|
||||
|
||||
async def create_session(self, client_id, data):
|
||||
"""Создание новой сессии (устройство)"""
|
||||
session_id = str(uuid.uuid4())
|
||||
device_info = data.get("device_info", {})
|
||||
|
||||
self.sessions[session_id] = {
|
||||
"device": self.clients[client_id],
|
||||
"operator": None,
|
||||
"device_info": device_info,
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Отправляем подтверждение устройству
|
||||
await self.clients[client_id].send(json.dumps({
|
||||
"type": "session_created",
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
logger.info(f"📱 Сессия создана: {session_id} для устройства {device_info.get('model', 'Unknown')}")
|
||||
|
||||
async def join_session(self, client_id, data):
|
||||
"""Подключение к сессии (оператор)"""
|
||||
session_id = data.get("session_id")
|
||||
|
||||
if session_id not in self.sessions:
|
||||
await self.clients[client_id].send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Session not found"
|
||||
}))
|
||||
return
|
||||
|
||||
session = self.sessions[session_id]
|
||||
if session["operator"] is not None:
|
||||
await self.clients[client_id].send(json.dumps({
|
||||
"type": "error",
|
||||
"message": "Session already has an operator"
|
||||
}))
|
||||
return
|
||||
|
||||
# Подключаем оператора
|
||||
session["operator"] = self.clients[client_id]
|
||||
|
||||
# Уведомляем оператора
|
||||
await self.clients[client_id].send(json.dumps({
|
||||
"type": "session_joined",
|
||||
"session_id": session_id,
|
||||
"device_info": session["device_info"],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
# Уведомляем устройство
|
||||
await session["device"].send(json.dumps({
|
||||
"type": "operator_joined",
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
logger.info(f"👤 Оператор подключен к сессии: {session_id}")
|
||||
|
||||
async def relay_offer(self, client_id, data):
|
||||
"""Передача SDP offer"""
|
||||
session_id = data.get("session_id")
|
||||
session = self.sessions.get(session_id)
|
||||
|
||||
if not session or session["operator"] is None:
|
||||
return
|
||||
|
||||
# Передаем offer оператору
|
||||
await session["operator"].send(json.dumps({
|
||||
"type": "offer",
|
||||
"session_id": session_id,
|
||||
"sdp": data.get("sdp"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
logger.info(f"📞 SDP Offer передан в сессии: {session_id}")
|
||||
|
||||
async def relay_answer(self, client_id, data):
|
||||
"""Передача SDP answer"""
|
||||
session_id = data.get("session_id")
|
||||
session = self.sessions.get(session_id)
|
||||
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Передаем answer устройству
|
||||
await session["device"].send(json.dumps({
|
||||
"type": "answer",
|
||||
"session_id": session_id,
|
||||
"sdp": data.get("sdp"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
logger.info(f"📞 SDP Answer передан в сессии: {session_id}")
|
||||
|
||||
async def relay_ice_candidate(self, client_id, data):
|
||||
"""Передача ICE кандидатов"""
|
||||
session_id = data.get("session_id")
|
||||
session = self.sessions.get(session_id)
|
||||
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Определяем кому передавать (устройству или оператору)
|
||||
if session["device"] == self.clients[client_id]:
|
||||
# От устройства к оператору
|
||||
if session["operator"]:
|
||||
await session["operator"].send(json.dumps({
|
||||
"type": "ice_candidate",
|
||||
"session_id": session_id,
|
||||
"candidate": data.get("candidate"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
else:
|
||||
# От оператора к устройству
|
||||
await session["device"].send(json.dumps({
|
||||
"type": "ice_candidate",
|
||||
"session_id": session_id,
|
||||
"candidate": data.get("candidate"),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
logger.info(f"🧊 ICE candidate передан в сессии: {session_id}")
|
||||
|
||||
async def handle_hangup(self, client_id, data):
|
||||
"""Завершение сессии"""
|
||||
session_id = data.get("session_id")
|
||||
session = self.sessions.get(session_id)
|
||||
|
||||
if not session:
|
||||
return
|
||||
|
||||
# Уведомляем обе стороны
|
||||
if session["device"]:
|
||||
await session["device"].send(json.dumps({
|
||||
"type": "hangup",
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
if session["operator"]:
|
||||
await session["operator"].send(json.dumps({
|
||||
"type": "hangup",
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
|
||||
# Удаляем сессию
|
||||
del self.sessions[session_id]
|
||||
logger.info(f"📴 Сессия завершена: {session_id}")
|
||||
|
||||
async def cleanup_client(self, client_id):
|
||||
"""Очистка при отключении клиента"""
|
||||
if client_id in self.clients:
|
||||
del self.clients[client_id]
|
||||
|
||||
# Удаляем сессии с этим клиентом
|
||||
sessions_to_remove = []
|
||||
for session_id, session in self.sessions.items():
|
||||
if (session["device"] == self.clients.get(client_id) or
|
||||
session["operator"] == self.clients.get(client_id)):
|
||||
sessions_to_remove.append(session_id)
|
||||
|
||||
for session_id in sessions_to_remove:
|
||||
del self.sessions[session_id]
|
||||
logger.info(f"🧹 Сессия {session_id} удалена из-за отключения клиента")
|
||||
|
||||
def main():
|
||||
"""Запуск сигналинг сервера"""
|
||||
server = SignalingServer()
|
||||
|
||||
print("🚀 GodEye Signaling Server")
|
||||
print("==========================")
|
||||
print("📡 Сервер запущен на ws://localhost:8765")
|
||||
print("💡 Для остановки нажмите Ctrl+C")
|
||||
print()
|
||||
|
||||
# Исправляем проблему с event loop
|
||||
async def run_server():
|
||||
start_server = websockets.serve(
|
||||
server.register_client,
|
||||
"0.0.0.0", # Слушаем на всех интерфейсах
|
||||
8765
|
||||
)
|
||||
await start_server
|
||||
await asyncio.Future() # Запускаем вечно
|
||||
|
||||
try:
|
||||
asyncio.run(run_server())
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Сервер остановлен")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
76
simple_test_server.py
Normal file
76
simple_test_server.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Простой тест для проверки автоподтверждения
|
||||
Имитирует сервер Socket.IO и отправляет запрос камеры
|
||||
"""
|
||||
import socket
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
class TestServer:
|
||||
def __init__(self, host='192.168.219.108', port=3002):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.clients = []
|
||||
|
||||
def start(self):
|
||||
print(f"🚀 Starting test server on {self.host}:{self.port}")
|
||||
print("📱 Configure Android app to connect to this server")
|
||||
print("🔄 Waiting for Android app connection...")
|
||||
|
||||
# Простой TCP сервер для имитации Socket.IO
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
try:
|
||||
server_socket.bind((self.host, self.port))
|
||||
server_socket.listen(5)
|
||||
|
||||
while True:
|
||||
client_socket, addr = server_socket.accept()
|
||||
print(f"📱 Android device connected from {addr}")
|
||||
|
||||
# Имитируем ответ Socket.IO
|
||||
response = "HTTP/1.1 200 OK\r\n"
|
||||
response += "Content-Type: application/json\r\n"
|
||||
response += "Access-Control-Allow-Origin: *\r\n\r\n"
|
||||
response += '{"status":"connected"}'
|
||||
|
||||
client_socket.send(response.encode())
|
||||
|
||||
# Отправляем запрос камеры через некоторое время
|
||||
time.sleep(3)
|
||||
self.send_camera_request(client_socket)
|
||||
|
||||
client_socket.close()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Stopping test server")
|
||||
finally:
|
||||
server_socket.close()
|
||||
|
||||
def send_camera_request(self, client_socket):
|
||||
camera_request = {
|
||||
"event": "camera-request",
|
||||
"data": {
|
||||
"sessionId": f"test_session_{int(time.time())}",
|
||||
"operatorId": "test_operator",
|
||||
"cameraType": "back",
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
print(f"📹 Sending camera request: {camera_request}")
|
||||
|
||||
try:
|
||||
message = json.dumps(camera_request) + "\n"
|
||||
client_socket.send(message.encode())
|
||||
print("✅ Camera request sent to Android app")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send camera request: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_server = TestServer()
|
||||
test_server.start()
|
||||
227
start_godeye.sh
Executable file
227
start_godeye.sh
Executable file
@@ -0,0 +1,227 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GodEye Complete Setup & Launch Script
|
||||
# Скрипт для запуска полной инфраструктуры GodEye
|
||||
|
||||
echo "🚀 GodEye Complete Infrastructure Setup"
|
||||
echo "======================================="
|
||||
|
||||
# Функция для получения IP адреса
|
||||
get_local_ip() {
|
||||
local ip=$(ip route get 1.1.1.1 | awk '{print $7}' | head -1)
|
||||
if [ -z "$ip" ]; then
|
||||
ip=$(hostname -I | awk '{print $1}')
|
||||
fi
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
# Получаем локальный IP
|
||||
LOCAL_IP=$(get_local_ip)
|
||||
echo "🌐 Локальный IP адрес: $LOCAL_IP"
|
||||
|
||||
# Проверяем зависимости
|
||||
echo ""
|
||||
echo "🔍 Проверка зависимостей..."
|
||||
|
||||
# Проверяем Python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "❌ Python3 не найден! Установите Python3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем websockets
|
||||
if ! python3 -c "import websockets" 2>/dev/null; then
|
||||
echo "📦 Установка websockets..."
|
||||
pip3 install websockets
|
||||
fi
|
||||
|
||||
# Проверяем adb
|
||||
if ! command -v adb &> /dev/null; then
|
||||
echo "❌ ADB не найден! Установите Android SDK Platform Tools"
|
||||
echo "sudo apt install android-tools-adb android-tools-fastboot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Все зависимости установлены"
|
||||
|
||||
# Функция для остановки всех процессов
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "🛑 Остановка всех сервисов..."
|
||||
|
||||
# Остановка сигналинг сервера
|
||||
if [ ! -z "$SIGNALING_PID" ]; then
|
||||
kill $SIGNALING_PID 2>/dev/null
|
||||
echo "📡 Сигналинг сервер остановлен"
|
||||
fi
|
||||
|
||||
# Остановка веб сервера
|
||||
if [ ! -z "$WEB_PID" ]; then
|
||||
kill $WEB_PID 2>/dev/null
|
||||
echo "🌐 Веб сервер остановлен"
|
||||
fi
|
||||
|
||||
echo "👋 Система остановлена"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Обработка сигнала прерывания
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
# Создаем директории для логов
|
||||
mkdir -p logs
|
||||
|
||||
echo ""
|
||||
echo "🔧 Запуск сервисов..."
|
||||
|
||||
# 1. Запуск сигналинг сервера
|
||||
echo "📡 Запуск сигналинг сервера на порту 8765..."
|
||||
python3 signaling_server.py > logs/signaling.log 2>&1 &
|
||||
SIGNALING_PID=$!
|
||||
|
||||
# Ждем запуска сигналинг сервера
|
||||
sleep 2
|
||||
|
||||
# Проверяем что сигналинг сервер запустился
|
||||
if ps -p $SIGNALING_PID > /dev/null; then
|
||||
echo "✅ Сигналинг сервер запущен (PID: $SIGNALING_PID)"
|
||||
else
|
||||
echo "❌ Ошибка запуска сигналинг сервера"
|
||||
cat logs/signaling.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Запуск простого веб сервера для интерфейса оператора
|
||||
echo "🌐 Запуск веб сервера на порту 8080..."
|
||||
python3 -m http.server 8080 > logs/webserver.log 2>&1 &
|
||||
WEB_PID=$!
|
||||
|
||||
# Ждем запуска веб сервера
|
||||
sleep 1
|
||||
|
||||
if ps -p $WEB_PID > /dev/null; then
|
||||
echo "✅ Веб сервер запущен (PID: $WEB_PID)"
|
||||
else
|
||||
echo "❌ Ошибка запуска веб сервера"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Обновляем IP адрес в веб интерфейсе
|
||||
echo "🔧 Обновление IP адреса в интерфейсе оператора..."
|
||||
sed -i "s/localhost:8765/${LOCAL_IP}:8765/g" operator-interface.html
|
||||
|
||||
# 4. Проверяем подключение Android устройства
|
||||
echo ""
|
||||
echo "📱 Проверка подключения Android устройства..."
|
||||
DEVICES=($(adb devices | grep -w "device" | awk '{print $1}'))
|
||||
|
||||
if [ ${#DEVICES[@]} -eq 0 ]; then
|
||||
echo "⚠️ Android устройства не найдены!"
|
||||
echo "Подключите устройство и включите отладку по USB"
|
||||
else
|
||||
echo "✅ Найдено устройств: ${#DEVICES[@]}"
|
||||
for device in "${DEVICES[@]}"; do
|
||||
echo " 📲 $device"
|
||||
done
|
||||
fi
|
||||
|
||||
# 5. Сборка и установка APK (если устройство подключено)
|
||||
if [ ${#DEVICES[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "🔨 Сборка и установка приложения..."
|
||||
|
||||
# Обновляем IP адрес в Android приложении
|
||||
SIGNALING_CLIENT_FILE="app/src/main/java/com/example/godeye/network/SignalingClient.kt"
|
||||
if [ -f "$SIGNALING_CLIENT_FILE" ]; then
|
||||
sed -i "s/192.168.1.100/${LOCAL_IP}/g" "$SIGNALING_CLIENT_FILE"
|
||||
echo "🔧 IP адрес обновлен в Android приложении: $LOCAL_IP"
|
||||
fi
|
||||
|
||||
# Собираем проект
|
||||
echo "⚙️ Сборка проекта..."
|
||||
./gradlew assembleDebug > logs/build.log 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Проект собран успешно"
|
||||
|
||||
# Устанавливаем на все подключенные устройства
|
||||
for device in "${DEVICES[@]}"; do
|
||||
echo "📲 Установка на устройство: $device"
|
||||
adb -s "$device" install -r app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Приложение установлено на $device"
|
||||
|
||||
# Запускаем приложение
|
||||
echo "🚀 Запуск приложения на $device"
|
||||
adb -s "$device" shell am start -n com.example.godeye/.MainActivity
|
||||
else
|
||||
echo "❌ Ошибка установки на $device"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "❌ Ошибка сборки проекта"
|
||||
echo "Проверьте logs/build.log для подробностей"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 6. Выводим информацию для подключения
|
||||
echo ""
|
||||
echo "🎯 Система запущена и готова к работе!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "📡 Сигналинг сервер: ws://${LOCAL_IP}:8765"
|
||||
echo "🌐 Интерфейс оператора: http://${LOCAL_IP}:8080/operator-interface.html"
|
||||
echo "📱 Android приложение: установлено и запущено на устройствах"
|
||||
echo ""
|
||||
echo "🔗 Инструкции по подключению:"
|
||||
echo "1. Откройте браузер и перейдите по адресу: http://${LOCAL_IP}:8080/operator-interface.html"
|
||||
echo "2. В браузере нажмите 'Подключиться к серверу'"
|
||||
echo "3. На Android устройстве запустите приложение GodEye"
|
||||
echo "4. В приложении нажмите 'Начать трансляцию'"
|
||||
echo "5. В браузере должно появиться устройство в списке доступных"
|
||||
echo "6. Нажмите на устройство для подключения"
|
||||
echo ""
|
||||
echo "📊 Мониторинг:"
|
||||
echo "• Логи сигналинга: tail -f logs/signaling.log"
|
||||
echo "• Логи веб сервера: tail -f logs/webserver.log"
|
||||
echo "• Логи сборки: tail -f logs/build.log"
|
||||
echo ""
|
||||
echo "🛑 Для остановки нажмите Ctrl+C"
|
||||
echo ""
|
||||
|
||||
# Основной цикл мониторинга
|
||||
echo "🔄 Мониторинг системы (каждые 10 сек)..."
|
||||
while true; do
|
||||
sleep 10
|
||||
|
||||
# Проверяем состояние сервисов
|
||||
SIGNALING_STATUS="❌"
|
||||
WEB_STATUS="❌"
|
||||
|
||||
if ps -p $SIGNALING_PID > /dev/null 2>&1; then
|
||||
SIGNALING_STATUS="✅"
|
||||
fi
|
||||
|
||||
if ps -p $WEB_PID > /dev/null 2>&1; then
|
||||
WEB_STATUS="✅"
|
||||
fi
|
||||
|
||||
# Проверяем подключенные устройства
|
||||
DEVICE_COUNT=$(adb devices | grep -w "device" | wc -l)
|
||||
|
||||
echo "$(date '+%H:%M:%S') - Статус: Сигналинг $SIGNALING_STATUS | Веб $WEB_STATUS | Устройств: $DEVICE_COUNT"
|
||||
|
||||
# Если какой-то сервис упал, пытаемся перезапустить
|
||||
if ! ps -p $SIGNALING_PID > /dev/null 2>&1; then
|
||||
echo "⚠️ Сигналинг сервер упал, перезапуск..."
|
||||
python3 signaling_server.py > logs/signaling.log 2>&1 &
|
||||
SIGNALING_PID=$!
|
||||
fi
|
||||
|
||||
if ! ps -p $WEB_PID > /dev/null 2>&1; then
|
||||
echo "⚠️ Веб сервер упал, перезапуск..."
|
||||
python3 -m http.server 8080 > logs/webserver.log 2>&1 &
|
||||
WEB_PID=$!
|
||||
fi
|
||||
done
|
||||
43
test_camera_request.py
Executable file
43
test_camera_request.py
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
import socketio
|
||||
import time
|
||||
import json
|
||||
|
||||
# Создаем Socket.IO клиент
|
||||
sio = socketio.SimpleClient()
|
||||
|
||||
def test_camera_request():
|
||||
try:
|
||||
# Подключаемся к серверу (используем правильный IP из тестов)
|
||||
print("Connecting to server...")
|
||||
sio.connect('http://192.168.219.108:3001')
|
||||
print("Connected!")
|
||||
|
||||
# Ждем немного для стабилизации соединения
|
||||
time.sleep(2)
|
||||
|
||||
# Отправляем запрос камеры
|
||||
camera_request = {
|
||||
"sessionId": f"test_session_{int(time.time())}",
|
||||
"operatorId": "test_operator",
|
||||
"cameraType": "back",
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}
|
||||
|
||||
print(f"Sending camera request: {camera_request}")
|
||||
sio.emit('camera-request', camera_request)
|
||||
|
||||
# Ждем ответа
|
||||
print("Waiting for response...")
|
||||
time.sleep(10)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
try:
|
||||
sio.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_camera_request()
|
||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
70
venv/bin/activate
Normal file
70
venv/bin/activate
Normal file
@@ -0,0 +1,70 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath /home/trevor/AndroidStudioProjects/GodEye/venv)
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV=/home/trevor/AndroidStudioProjects/GodEye/venv
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(venv) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(venv) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
27
venv/bin/activate.csh
Normal file
27
venv/bin/activate.csh
Normal file
@@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /home/trevor/AndroidStudioProjects/GodEye/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = '(venv) '"$prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
69
venv/bin/activate.fish
Normal file
69
venv/bin/activate.fish
Normal file
@@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /home/trevor/AndroidStudioProjects/GodEye/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||
end
|
||||
8
venv/bin/flask
Executable file
8
venv/bin/flask
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from flask.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip
Executable file
8
venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3
Executable file
8
venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
8
venv/bin/pip3.12
Executable file
8
venv/bin/pip3.12
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
venv/bin/python3.12
Symbolic link
1
venv/bin/python3.12
Symbolic link
@@ -0,0 +1 @@
|
||||
python3
|
||||
8
venv/bin/websockets
Executable file
8
venv/bin/websockets
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/home/trevor/AndroidStudioProjects/GodEye/venv/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
164
venv/include/site/python3.12/greenlet/greenlet.h
Normal file
164
venv/include/site/python3.12/greenlet/greenlet.h
Normal file
@@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,376 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
Copyright 2009-2024 Joshua Bronson. All rights reserved.
|
||||
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user