Skip to content

Commit 56159e7

Browse files
committed
feat: implement server toggle functionality and update stream status management
1 parent 773b48f commit 56159e7

2 files changed

Lines changed: 182 additions & 64 deletions

File tree

app/src/main/java/com/raulshma/lenscast/camera/CameraScreen.kt

Lines changed: 131 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import androidx.compose.material.icons.filled.Bolt
4747
import androidx.compose.material.icons.filled.Camera
4848
import androidx.compose.material.icons.filled.CameraAlt
4949
import androidx.compose.material.icons.filled.Cameraswitch
50+
import androidx.compose.material.icons.filled.Check
5051
import androidx.compose.material.icons.filled.Collections
5152
import androidx.compose.material.icons.filled.ContentCopy
5253
import androidx.compose.material.icons.filled.Exposure
@@ -65,6 +66,7 @@ import androidx.compose.material.icons.filled.Videocam
6566
import androidx.compose.material.icons.filled.Visibility
6667
import androidx.compose.material.icons.filled.VisibilityOff
6768
import androidx.compose.material.icons.filled.Wifi
69+
import androidx.compose.material.icons.filled.WifiOff
6870
import androidx.compose.material.icons.filled.WbSunny
6971
import androidx.compose.material.icons.filled.ZoomIn
7072
import androidx.compose.material3.Button
@@ -243,6 +245,7 @@ fun CameraScreen(
243245
onNavigateToCapture = onNavigateToCapture,
244246
onNavigateToSettings = onNavigateToSettings,
245247
onCopyStreamUrl = { viewModel.copyStreamUrl() },
248+
onToggleServer = { viewModel.toggleServer() },
246249
onSelectLens = { viewModel.selectLens(it) },
247250
)
248251

@@ -294,6 +297,7 @@ private fun ImmersiveCameraView(
294297
onNavigateToCapture: () -> Unit,
295298
onNavigateToSettings: () -> Unit,
296299
onCopyStreamUrl: () -> Unit,
300+
onToggleServer: () -> Unit,
297301
onSelectLens: (Int) -> Unit,
298302
) {
299303
Box(modifier = Modifier.fillMaxSize()) {
@@ -369,6 +373,8 @@ private fun ImmersiveCameraView(
369373
onNavigateToCapture = onNavigateToCapture,
370374
onNavigateToSettings = onNavigateToSettings,
371375
onToggleQuickSettings = onToggleQuickSettings,
376+
onCopyStreamUrl = onCopyStreamUrl,
377+
onToggleServer = onToggleServer,
372378
modifier = Modifier
373379
.align(Alignment.TopStart)
374380
.fillMaxWidth()
@@ -417,7 +423,6 @@ private fun ImmersiveCameraView(
417423
onStreamToggle = onStreamToggle,
418424
onCapture = onCapture,
419425
onRecord = onRecord,
420-
onCopyStreamUrl = onCopyStreamUrl,
421426
onSelectLens = onSelectLens,
422427
onToggleQuickSettings = onToggleQuickSettings,
423428
onQuickSettingTap = onQuickSettingTap,
@@ -451,6 +456,8 @@ private fun CameraTopOverlay(
451456
onNavigateToCapture: () -> Unit,
452457
onNavigateToSettings: () -> Unit,
453458
onToggleQuickSettings: () -> Unit,
459+
onCopyStreamUrl: () -> Unit,
460+
onToggleServer: () -> Unit,
454461
modifier: Modifier = Modifier,
455462
) {
456463
var menuExpanded by remember { mutableStateOf(false) }
@@ -481,6 +488,11 @@ private fun CameraTopOverlay(
481488
horizontalArrangement = Arrangement.spacedBy(2.dp),
482489
verticalAlignment = Alignment.CenterVertically
483490
) {
491+
ServerStatusButton(
492+
streamStatus = streamStatus,
493+
onCopyUrl = onCopyStreamUrl,
494+
onToggleServer = onToggleServer,
495+
)
484496
CameraControlButton(
485497
icon = if (showPreview) Icons.Default.Visibility else Icons.Default.VisibilityOff,
486498
contentDescription = if (showPreview) "Hide preview" else "Show preview",
@@ -609,7 +621,6 @@ private fun CameraBottomOverlay(
609621
onStreamToggle: () -> Unit,
610622
onCapture: () -> Unit,
611623
onRecord: () -> Unit,
612-
onCopyStreamUrl: () -> Unit,
613624
onSelectLens: (Int) -> Unit,
614625
onToggleQuickSettings: () -> Unit,
615626
onQuickSettingTap: (QuickSettingType) -> Unit,
@@ -637,15 +648,6 @@ private fun CameraBottomOverlay(
637648
)
638649
}
639650

640-
if (streamStatus.isServerRunning && streamStatus.url.isNotBlank()) {
641-
StreamInfoBar(
642-
url = streamStatus.url,
643-
isStreaming = streamStatus.isActive,
644-
clientCount = streamStatus.clientCount,
645-
onCopyUrl = onCopyStreamUrl
646-
)
647-
}
648-
649651
if (availableLenses.size > 1) {
650652
LensSelectorRow(
651653
lenses = availableLenses,
@@ -1393,66 +1395,131 @@ private fun RecordingIndicator(
13931395
}
13941396

13951397
@Composable
1396-
private fun StreamInfoBar(
1397-
url: String,
1398-
isStreaming: Boolean,
1399-
clientCount: Int,
1398+
private fun ServerStatusButton(
1399+
streamStatus: com.raulshma.lenscast.camera.model.StreamStatus,
14001400
onCopyUrl: () -> Unit,
1401+
onToggleServer: () -> Unit,
1402+
modifier: Modifier = Modifier,
14011403
) {
1402-
Surface(
1403-
modifier = Modifier.fillMaxWidth(),
1404-
color = OverlayScrim,
1405-
shape = RoundedCornerShape(0.dp)
1406-
) {
1407-
Row(
1408-
modifier = Modifier
1409-
.fillMaxWidth()
1410-
.padding(horizontal = 16.dp, vertical = 8.dp),
1411-
verticalAlignment = Alignment.CenterVertically
1404+
var expanded by remember { mutableStateOf(false) }
1405+
1406+
val iconTint by animateColorAsState(
1407+
targetValue = when {
1408+
streamStatus.isActive -> Color(0xFF4CAF50)
1409+
streamStatus.isServerRunning -> MaterialTheme.colorScheme.primary
1410+
else -> Color.White.copy(alpha = 0.4f)
1411+
},
1412+
animationSpec = tween(300),
1413+
label = "server_status_tint"
1414+
)
1415+
1416+
Box(modifier = modifier) {
1417+
CameraControlButton(
1418+
icon = Icons.Default.Wifi,
1419+
contentDescription = "Web server status",
1420+
onClick = { expanded = true },
1421+
tint = iconTint,
1422+
)
1423+
DropdownMenu(
1424+
expanded = expanded,
1425+
onDismissRequest = { expanded = false },
1426+
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
1427+
shape = RoundedCornerShape(16.dp),
14121428
) {
1413-
Icon(
1414-
imageVector = Icons.Default.Wifi,
1415-
contentDescription = null,
1416-
modifier = Modifier.size(16.dp),
1417-
tint = MaterialTheme.colorScheme.primary
1429+
DropdownMenuItem(
1430+
text = {
1431+
Column {
1432+
Text(
1433+
"Web Server",
1434+
style = MaterialTheme.typography.labelLarge,
1435+
fontWeight = FontWeight.Bold,
1436+
)
1437+
Spacer(modifier = Modifier.height(4.dp))
1438+
if (streamStatus.url.isNotBlank()) {
1439+
Text(
1440+
streamStatus.url,
1441+
style = MaterialTheme.typography.bodySmall.copy(
1442+
fontFamily = FontFamily.Monospace
1443+
),
1444+
maxLines = 1,
1445+
)
1446+
} else {
1447+
Text(
1448+
"Server offline",
1449+
style = MaterialTheme.typography.bodySmall,
1450+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
1451+
)
1452+
}
1453+
Spacer(modifier = Modifier.height(2.dp))
1454+
Text(
1455+
when {
1456+
streamStatus.clientCount > 0 -> "${streamStatus.clientCount} viewer(s) connected"
1457+
streamStatus.isActive -> "Live stream active"
1458+
streamStatus.isServerRunning -> "Server ready"
1459+
else -> "Offline"
1460+
},
1461+
style = MaterialTheme.typography.labelSmall,
1462+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
1463+
)
1464+
}
1465+
},
1466+
onClick = { expanded = false },
1467+
enabled = false,
14181468
)
1419-
Spacer(modifier = Modifier.size(8.dp))
1420-
Column(modifier = Modifier.weight(1f)) {
1421-
Text(
1422-
text = url,
1423-
style = MaterialTheme.typography.labelSmall.copy(
1424-
fontFamily = FontFamily.Monospace
1425-
),
1426-
maxLines = 1,
1427-
color = Color.White.copy(alpha = 0.9f)
1428-
)
1429-
Text(
1430-
text = when {
1431-
clientCount > 0 -> "$clientCount viewer(s)"
1432-
isStreaming -> "Live stream active"
1433-
else -> "Web server ready"
1469+
if (streamStatus.url.isNotBlank()) {
1470+
DropdownMenuItem(
1471+
text = {
1472+
Row(verticalAlignment = Alignment.CenterVertically) {
1473+
Icon(
1474+
Icons.Default.ContentCopy,
1475+
contentDescription = null,
1476+
modifier = Modifier.size(18.dp),
1477+
tint = MaterialTheme.colorScheme.onSurface,
1478+
)
1479+
Spacer(modifier = Modifier.width(12.dp))
1480+
Text("Copy URL", style = MaterialTheme.typography.bodyMedium)
1481+
}
1482+
},
1483+
onClick = {
1484+
expanded = false
1485+
onCopyUrl()
14341486
},
1435-
style = MaterialTheme.typography.labelSmall,
1436-
color = Color.White.copy(alpha = 0.6f)
14371487
)
14381488
}
1439-
Surface(
1440-
color = OverlayLight,
1441-
shape = CircleShape,
1442-
onClick = onCopyUrl
1443-
) {
1444-
Box(
1445-
modifier = Modifier.size(32.dp),
1446-
contentAlignment = Alignment.Center
1447-
) {
1448-
Icon(
1449-
imageVector = Icons.Default.ContentCopy,
1450-
contentDescription = "Copy URL",
1451-
modifier = Modifier.size(16.dp),
1452-
tint = Color.White.copy(alpha = 0.8f)
1453-
)
1454-
}
1455-
}
1489+
DropdownMenuItem(
1490+
text = {
1491+
Row(
1492+
verticalAlignment = Alignment.CenterVertically,
1493+
modifier = Modifier.fillMaxWidth(),
1494+
horizontalArrangement = Arrangement.SpaceBetween,
1495+
) {
1496+
Row(verticalAlignment = Alignment.CenterVertically) {
1497+
Icon(
1498+
if (streamStatus.isServerRunning) Icons.Default.Wifi else Icons.Default.WifiOff,
1499+
contentDescription = null,
1500+
modifier = Modifier.size(18.dp),
1501+
tint = MaterialTheme.colorScheme.onSurface,
1502+
)
1503+
Spacer(modifier = Modifier.width(12.dp))
1504+
Text(
1505+
if (streamStatus.isServerRunning) "Turn off server" else "Turn on server",
1506+
style = MaterialTheme.typography.bodyMedium,
1507+
)
1508+
}
1509+
Switch(
1510+
checked = streamStatus.isServerRunning,
1511+
onCheckedChange = null,
1512+
thumbContent = if (streamStatus.isServerRunning) {
1513+
{ Icon(Icons.Default.Check, null, Modifier.size(12.dp)) }
1514+
} else null,
1515+
)
1516+
}
1517+
},
1518+
onClick = {
1519+
expanded = false
1520+
onToggleServer()
1521+
},
1522+
)
14561523
}
14571524
}
14581525
}

app/src/main/java/com/raulshma/lenscast/camera/CameraViewModel.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,57 @@ class CameraViewModel(
426426
}
427427
}
428428

429+
fun toggleServer() {
430+
if (_streamStatus.value.isServerRunning) {
431+
stopServer()
432+
} else {
433+
startServer()
434+
}
435+
}
436+
437+
private fun startServer() {
438+
val started = streamingManager.ensureServerRunning()
439+
if (started) {
440+
_streamStatus.value = _streamStatus.value.copy(
441+
isServerRunning = true,
442+
url = streamingManager.streamUrl.value,
443+
)
444+
} else {
445+
_streamStatus.value = _streamStatus.value.copy(
446+
isServerRunning = false,
447+
url = "Failed to start server",
448+
)
449+
}
450+
}
451+
452+
private fun stopServer() {
453+
if (_streamStatus.value.isActive) {
454+
streamingManager.stopStreaming()
455+
powerManager.releaseWakeLock()
456+
thermalMonitor.stopMonitoring()
457+
batteryMonitorJob?.cancel()
458+
batteryMonitorJob = null
459+
thermalMonitorJob?.cancel()
460+
thermalMonitorJob = null
461+
cameraService.releaseKeepAlive()
462+
cameraService.rebindUseCases()
463+
464+
val intent = Intent(context, com.raulshma.lenscast.streaming.StreamingService::class.java)
465+
intent.action = com.raulshma.lenscast.streaming.StreamingService.ACTION_PAUSE
466+
context.startService(intent)
467+
} else {
468+
streamingManager.stopStreaming()
469+
}
470+
_streamStatus.value = _streamStatus.value.copy(
471+
isActive = false,
472+
isServerRunning = false,
473+
url = "",
474+
clientCount = 0,
475+
isAudioActive = false,
476+
audioUrl = "",
477+
)
478+
}
479+
429480
fun copyStreamUrl() {
430481
val url = _streamStatus.value.url.ifEmpty { streamingManager.streamUrl.value }
431482
if (url.isNotEmpty()) {

0 commit comments

Comments
 (0)