|
|
|
|
@ -6,6 +6,9 @@ import android.content.pm.PackageManager
|
|
|
|
|
import android.content.Context |
|
|
|
|
import android.graphics.Matrix |
|
|
|
|
import android.graphics.SurfaceTexture |
|
|
|
|
import android.hardware.display.DisplayManager |
|
|
|
|
import android.os.Handler |
|
|
|
|
import android.os.Looper |
|
|
|
|
import android.os.SystemClock |
|
|
|
|
import android.view.Surface |
|
|
|
|
import android.view.TextureView |
|
|
|
|
@ -69,6 +72,7 @@ import kotlinx.coroutines.withContext
|
|
|
|
|
fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSource>, selected: StreamSource?, |
|
|
|
|
sourcesFailed: Boolean, onSelect: (StreamSource?) -> Unit, onBack: () -> Unit) { |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val displayDegrees = StreamDisplayDegrees() |
|
|
|
|
val group = remember(channelId) { channelId.toULongOrNull()?.toLong() ?: 0L } |
|
|
|
|
val state by StreamController.state.collectAsState() |
|
|
|
|
val local = state?.takeIf { it.group == group } |
|
|
|
|
@ -117,7 +121,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
|
|
|
|
|
if (selected != null) { |
|
|
|
|
Text(selected.displayName, style = MaterialTheme.typography.titleSmall) |
|
|
|
|
if (selected.self && local?.ongoing == true) { |
|
|
|
|
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null, |
|
|
|
|
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null, displayDegrees, |
|
|
|
|
StreamController::change) |
|
|
|
|
CaptureModeStatus(local.config, local.pendingConfig, local.notice) |
|
|
|
|
StreamTxPanel(local.id) |
|
|
|
|
@ -125,7 +129,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
|
|
|
|
|
Box(Modifier.weight(1f).fillMaxWidth()) { |
|
|
|
|
if (selected.self && local?.ongoing != true) Text(stringResource(R.string.stream_ended)) |
|
|
|
|
else key(group, selected.id, selected.epoch) { |
|
|
|
|
LiveStreamView(group, selected.id, selected.epoch, if (selected.self) local?.preview else null) |
|
|
|
|
LiveStreamView(group, selected.id, selected.epoch, if (selected.self) local?.preview else null, displayDegrees) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
TextButton(onClick = { onSelect(null); onBack() }) { Text(stringResource(R.string.stream_close_view)) } |
|
|
|
|
@ -133,7 +137,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
|
|
|
|
|
enabled = local.phase != StreamPhase.STOPPING) { Text(stringResource(R.string.stream_stop)) } |
|
|
|
|
} else { |
|
|
|
|
if (local?.ongoing == true) { |
|
|
|
|
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null, |
|
|
|
|
CaptureOptions(cameras, local.pendingConfig ?: local.config, local.phase == StreamPhase.LIVE && local.pendingConfig == null, displayDegrees, |
|
|
|
|
StreamController::change) |
|
|
|
|
CaptureModeStatus(local.config, local.pendingConfig, local.notice) |
|
|
|
|
StreamTxPanel(local.id) |
|
|
|
|
@ -144,7 +148,7 @@ fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSou
|
|
|
|
|
})) |
|
|
|
|
Button(onClick = StreamController::stop, enabled = local.phase != StreamPhase.STOPPING) { Text(stringResource(R.string.stream_stop)) } |
|
|
|
|
} else { |
|
|
|
|
CaptureOptions(cameras, config, !loadingModes) { mode = it.mode; orientation = it.orientation; videoBitrate = it.videoBitrate } |
|
|
|
|
CaptureOptions(cameras, config, !loadingModes, displayDegrees) { mode = it.mode; orientation = it.orientation; videoBitrate = it.videoBitrate } |
|
|
|
|
if (!loadingModes && cameras.isEmpty()) Text(stringResource(R.string.stream_no_modes), color = MaterialTheme.colorScheme.error) |
|
|
|
|
Row { Checkbox(checked = audio, onCheckedChange = { audio = it }); Text(stringResource(R.string.microphone), Modifier.padding(top = 12.dp)) } |
|
|
|
|
Button(onClick = { |
|
|
|
|
@ -188,7 +192,7 @@ private fun StreamTxPanel(id: Long) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
|
private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureConfig?, enabled: Boolean, |
|
|
|
|
private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureConfig?, enabled: Boolean, display: Int, |
|
|
|
|
onChange: (StreamCaptureConfig) -> Unit) { |
|
|
|
|
val mode = config?.mode |
|
|
|
|
Row(Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
|
|
|
|
@ -239,9 +243,6 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val configuration = LocalConfiguration.current |
|
|
|
|
val display = remember(configuration) { ((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: 0) * 90 } |
|
|
|
|
var expanded by remember { mutableStateOf(false) } |
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { |
|
|
|
|
Box { |
|
|
|
|
@ -295,7 +296,7 @@ private fun StreamRotationMark(top: Int) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
|
private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?) { |
|
|
|
|
private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?, displayDegrees: Int) { |
|
|
|
|
val owner = LocalLifecycleOwner.current |
|
|
|
|
var visible by remember(owner) { mutableStateOf(owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) } |
|
|
|
|
DisposableEffect(owner, localPreview) { |
|
|
|
|
@ -307,17 +308,35 @@ private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview:
|
|
|
|
|
owner.lifecycle.addObserver(observer) |
|
|
|
|
onDispose { owner.lifecycle.removeObserver(observer); localPreview?.setVisible(false) } |
|
|
|
|
} |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val configuration = LocalConfiguration.current |
|
|
|
|
val displayDegrees = if (localPreview != null) remember(configuration) { |
|
|
|
|
((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: Surface.ROTATION_0) * 90 |
|
|
|
|
} else 0 |
|
|
|
|
if (visible) { |
|
|
|
|
if (localPreview != null) CameraPreviewSurface(localPreview, displayDegrees) |
|
|
|
|
else LiveStreamSurface(group, source, epoch) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** DisplayListener замечает также 180°, когда Configuration и размеры окна не меняются. */ |
|
|
|
|
@Composable |
|
|
|
|
private fun StreamDisplayDegrees(): Int { |
|
|
|
|
val context = LocalContext.current |
|
|
|
|
val configuration = LocalConfiguration.current |
|
|
|
|
val display = (context as? Activity)?.windowManager?.defaultDisplay |
|
|
|
|
var degrees by remember(display) { mutableStateOf((display?.rotation ?: 0) * 90) } |
|
|
|
|
DisposableEffect(context, display, configuration) { |
|
|
|
|
degrees = (display?.rotation ?: 0) * 90 |
|
|
|
|
val manager = context.getSystemService(DisplayManager::class.java) |
|
|
|
|
val listener = object : DisplayManager.DisplayListener { |
|
|
|
|
override fun onDisplayAdded(id: Int) {} |
|
|
|
|
override fun onDisplayRemoved(id: Int) {} |
|
|
|
|
override fun onDisplayChanged(id: Int) { |
|
|
|
|
if (id == display?.displayId) degrees = display.rotation * 90 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
manager.registerDisplayListener(listener, Handler(Looper.getMainLooper())) |
|
|
|
|
onDispose { manager.unregisterDisplayListener(listener) } |
|
|
|
|
} |
|
|
|
|
return degrees |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
|
private fun LiveStreamSurface(group: Long, source: Long, epoch: Long) { |
|
|
|
|
val engine = remember(group, source, epoch) { StreamPlayerEngine(group, source, epoch) } |
|
|
|
|
@ -356,7 +375,7 @@ private fun CameraPreviewSurface(preview: StreamPreview, displayDegrees: Int) {
|
|
|
|
|
val state by preview.state.collectAsState() |
|
|
|
|
val aspect = state.format?.aspect(displayDegrees) ?: 9f / 16f |
|
|
|
|
Column(Modifier.fillMaxSize()) { |
|
|
|
|
Text(stringResource(if (state.failed) R.string.stream_failed else if (state.active) R.string.stream_live else R.string.stream_connecting)) |
|
|
|
|
Text(stringResource(if (state.failed) R.string.stream_preview_failed else if (state.active) R.string.stream_live else R.string.stream_connecting)) |
|
|
|
|
Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) { |
|
|
|
|
Box(Modifier.aspectRatio(aspect, matchHeightConstraintsFirst = true)) { |
|
|
|
|
AndroidView(factory = { CameraPreviewView(it, preview) }, modifier = Modifier.matchParentSize(), |
|
|
|
|
@ -375,13 +394,14 @@ private class CameraPreviewView(context: Context, private val preview: StreamPre
|
|
|
|
|
private var frames = 0L |
|
|
|
|
private var reportFrames = 0L |
|
|
|
|
private var reportNs = 0L |
|
|
|
|
private var observedFormat: StreamPreviewFormat? = null |
|
|
|
|
|
|
|
|
|
init { |
|
|
|
|
keepScreenOn = true |
|
|
|
|
surfaceTextureListener = object : SurfaceTextureListener { |
|
|
|
|
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) { |
|
|
|
|
if (closed) return |
|
|
|
|
target = preview.attach(texture); frames = 0; reportFrames = 0; reportNs = System.nanoTime() |
|
|
|
|
target = preview.attach(texture); frames = 0; reportFrames = 0; reportNs = System.nanoTime(); observedFormat = null |
|
|
|
|
transform() |
|
|
|
|
} |
|
|
|
|
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { transform() } |
|
|
|
|
@ -393,6 +413,12 @@ private class CameraPreviewView(context: Context, private val preview: StreamPre
|
|
|
|
|
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) { |
|
|
|
|
val timestamp = texture.timestamp |
|
|
|
|
if (timestamp == 0L) return // Очистка TextureView до первого кадра камеры. |
|
|
|
|
if (format != observedFormat) { |
|
|
|
|
val native = FloatArray(16); texture.getTransformMatrix(native) |
|
|
|
|
LogManager.addLog("INFO", "video", "direct preview native transform sensor=${format?.camera?.sensor} " + |
|
|
|
|
"front=${format?.camera?.front} native=${listOf(native[0], native[1], native[4], native[5])}") |
|
|
|
|
observedFormat = format |
|
|
|
|
} |
|
|
|
|
frames++ |
|
|
|
|
val now = System.nanoTime() |
|
|
|
|
if (frames == 1L || now - reportNs >= 1_000_000_000) { |
|
|
|
|
@ -401,7 +427,7 @@ private class CameraPreviewView(context: Context, private val preview: StreamPre
|
|
|
|
|
LogManager.addLog(if (frames == 1L) "INFO" else "DEBUG", "video", "direct preview frame " + |
|
|
|
|
"target=${target?.let(System::identityHashCode)} frames=$frames fps=${"%.1f".format(fps)} " + |
|
|
|
|
"ageUs=$age timestampNs=$timestamp realtime=${format?.realtimeTimestamp} " + |
|
|
|
|
"rotation=${format?.camera?.rotation(displayDegrees)}") |
|
|
|
|
"sensor=${format?.camera?.sensor} display=$displayDegrees textureRotation=${format?.textureRotation(displayDegrees)}") |
|
|
|
|
reportNs = now; reportFrames = frames |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -425,7 +451,7 @@ private class CameraPreviewView(context: Context, private val preview: StreamPre
|
|
|
|
|
} |
|
|
|
|
setTransform(matrix) |
|
|
|
|
LogManager.addLog("DEBUG", "video", "direct preview transform view=${width}x$height buffer=${next.width}x${next.height} " + |
|
|
|
|
"rotation=${next.camera.rotation(displayDegrees)} front=${next.camera.front}") |
|
|
|
|
"sensor=${next.camera.sensor} display=$displayDegrees textureRotation=${next.textureRotation(displayDegrees)} front=${next.camera.front}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun close() { closed = true; keepScreenOn = false; target?.let(preview::detach) } |
|
|
|
|
|