|
|
|
@ -32,23 +32,21 @@ import com.utun.chat.data.NativeLib |
|
|
|
import com.utun.chat.data.StreamController |
|
|
|
import com.utun.chat.data.StreamController |
|
|
|
import com.utun.chat.data.StreamPhase |
|
|
|
import com.utun.chat.data.StreamPhase |
|
|
|
import com.utun.chat.data.StreamPlayerEngine |
|
|
|
import com.utun.chat.data.StreamPlayerEngine |
|
|
|
|
|
|
|
import com.utun.chat.data.StreamSource |
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
import kotlinx.coroutines.delay |
|
|
|
import kotlinx.coroutines.delay |
|
|
|
import kotlinx.coroutines.withContext |
|
|
|
import kotlinx.coroutines.withContext |
|
|
|
import org.json.JSONArray |
|
|
|
import org.json.JSONArray |
|
|
|
|
|
|
|
|
|
|
|
private data class LiveSource(val id: Long, val name: String, val epoch: String, val self: Boolean) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Выход с экрана закрывает только reader; foreground publisher останавливает явная кнопка/notification. */ |
|
|
|
/** Выход с экрана закрывает только reader; foreground publisher останавливает явная кнопка/notification. */ |
|
|
|
@OptIn(ExperimentalMaterial3Api::class) |
|
|
|
@OptIn(ExperimentalMaterial3Api::class) |
|
|
|
@Composable |
|
|
|
@Composable |
|
|
|
fun StreamScreen(channelId: String, channelName: String, onBack: () -> Unit) { |
|
|
|
fun StreamScreen(channelId: String, channelName: String, sources: List<StreamSource>, selected: StreamSource?, |
|
|
|
|
|
|
|
sourcesFailed: Boolean, onSelect: (StreamSource?) -> Unit, onBack: () -> Unit) { |
|
|
|
val context = LocalContext.current |
|
|
|
val context = LocalContext.current |
|
|
|
val group = remember(channelId) { channelId.toULongOrNull()?.toLong() ?: 0L } |
|
|
|
val group = remember(channelId) { channelId.toULongOrNull()?.toLong() ?: 0L } |
|
|
|
val state by StreamController.state.collectAsState() |
|
|
|
val state by StreamController.state.collectAsState() |
|
|
|
val local = state?.takeIf { it.group == group } |
|
|
|
val local = state?.takeIf { it.group == group } |
|
|
|
var sources by remember(group) { mutableStateOf<List<LiveSource>>(emptyList()) } |
|
|
|
|
|
|
|
var selected by remember(group) { mutableStateOf<LiveSource?>(null) } |
|
|
|
|
|
|
|
var previewedSession by remember(group) { mutableStateOf<Long?>(null) } |
|
|
|
var previewedSession by remember(group) { mutableStateOf<Long?>(null) } |
|
|
|
var audio by remember { mutableStateOf(true) } |
|
|
|
var audio by remember { mutableStateOf(true) } |
|
|
|
var camera by remember { mutableStateOf("") } |
|
|
|
var camera by remember { mutableStateOf("") } |
|
|
|
@ -64,24 +62,10 @@ fun StreamScreen(channelId: String, channelName: String, onBack: () -> Unit) { |
|
|
|
val cameraGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED } |
|
|
|
val cameraGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED } |
|
|
|
val micGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED } |
|
|
|
val micGranted = permissionVersion.let { ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED } |
|
|
|
|
|
|
|
|
|
|
|
LaunchedEffect(group) { |
|
|
|
|
|
|
|
while (true) { |
|
|
|
|
|
|
|
if (NativeLib.isReady()) sources = withContext(Dispatchers.IO) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
val json = JSONArray(NativeLib.streamSources(group)) |
|
|
|
|
|
|
|
(0 until json.length()).map { i -> |
|
|
|
|
|
|
|
val member = json.getJSONObject(i) |
|
|
|
|
|
|
|
LiveSource(member.getString("id").toULong().toLong(), member.getString("name"), member.getString("epoch"), member.optInt("self") == 1) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} catch (e: Exception) { LogManager.addLog("WARN", "stream", "stream list: ${e.message}"); emptyList() } |
|
|
|
|
|
|
|
} else sources = emptyList() |
|
|
|
|
|
|
|
delay(1000) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
LaunchedEffect(sources, local?.id, local?.phase) { |
|
|
|
LaunchedEffect(sources, local?.id, local?.phase) { |
|
|
|
if (local != null && !local.ongoing && selected?.self == true) selected = null |
|
|
|
if (local != null && !local.ongoing && selected?.self == true) onSelect(null) |
|
|
|
if (local?.phase == StreamPhase.LIVE && previewedSession != local.id) sources.firstOrNull { it.self }?.let { |
|
|
|
if (local?.phase == StreamPhase.LIVE && previewedSession != local.id) sources.firstOrNull { it.self }?.let { |
|
|
|
selected = it; previewedSession = local.id |
|
|
|
onSelect(it); previewedSession = local.id |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -92,6 +76,14 @@ fun StreamScreen(channelId: String, channelName: String, onBack: () -> Unit) { |
|
|
|
}) { padding -> |
|
|
|
}) { padding -> |
|
|
|
Column(Modifier.fillMaxSize().padding(padding).padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { |
|
|
|
Column(Modifier.fillMaxSize().padding(padding).padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { |
|
|
|
Text(channelName, style = MaterialTheme.typography.titleMedium) |
|
|
|
Text(channelName, style = MaterialTheme.typography.titleMedium) |
|
|
|
|
|
|
|
if (selected != null) { |
|
|
|
|
|
|
|
Text(selected.displayName, style = MaterialTheme.typography.titleSmall) |
|
|
|
|
|
|
|
Box(Modifier.weight(1f).fillMaxWidth()) { |
|
|
|
|
|
|
|
key(group, selected.id, selected.epoch) { LiveStreamView(group, selected.id, selected.epoch, selected.self) } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
TextButton(onClick = { onSelect(null); onBack() }) { Text(stringResource(R.string.stream_close_view)) } |
|
|
|
|
|
|
|
return@Column |
|
|
|
|
|
|
|
} |
|
|
|
if (local?.ongoing == true) { |
|
|
|
if (local?.ongoing == true) { |
|
|
|
Text(stringResource(when (local.phase) { |
|
|
|
Text(stringResource(when (local.phase) { |
|
|
|
StreamPhase.LIVE -> R.string.stream_live |
|
|
|
StreamPhase.LIVE -> R.string.stream_live |
|
|
|
@ -117,24 +109,19 @@ fun StreamScreen(channelId: String, channelName: String, onBack: () -> Unit) { |
|
|
|
}, enabled = camera.isNotEmpty() && group != 0L && state?.ongoing != true) { Text(stringResource(R.string.stream_start)) } |
|
|
|
}, enabled = camera.isNotEmpty() && group != 0L && state?.ongoing != true) { Text(stringResource(R.string.stream_start)) } |
|
|
|
} |
|
|
|
} |
|
|
|
local?.error?.let { Text(stringResource(it), color = MaterialTheme.colorScheme.error) } |
|
|
|
local?.error?.let { Text(stringResource(it), color = MaterialTheme.colorScheme.error) } |
|
|
|
if (sources.isEmpty() && local?.ongoing != true) Text(stringResource(R.string.stream_none)) |
|
|
|
if (sourcesFailed) Text(stringResource(R.string.stream_list_failed), color = MaterialTheme.colorScheme.error) |
|
|
|
|
|
|
|
else if (sources.isEmpty() && local?.ongoing != true) Text(stringResource(R.string.stream_none)) |
|
|
|
Column(Modifier.fillMaxWidth().heightIn(max = 160.dp).verticalScroll(rememberScrollState())) { |
|
|
|
Column(Modifier.fillMaxWidth().heightIn(max = 160.dp).verticalScroll(rememberScrollState())) { |
|
|
|
sources.forEach { source -> |
|
|
|
sources.forEach { source -> |
|
|
|
TextButton(onClick = { selected = source }) { Text(stringResource(R.string.stream_watch, source.name.ifEmpty { source.id.toULong().toString() })) } |
|
|
|
TextButton(onClick = { onSelect(source) }) { Text(stringResource(R.string.stream_watch, source.displayName)) } |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
selected?.let { source -> |
|
|
|
|
|
|
|
Box(Modifier.weight(1f).fillMaxWidth()) { |
|
|
|
|
|
|
|
key(group, source.id, source.epoch) { LiveStreamView(group, source.id, source.self) } |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
TextButton(onClick = { selected = null }) { Text(stringResource(R.string.stream_close_view)) } |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
@Composable |
|
|
|
private fun LiveStreamView(group: Long, source: Long, localPreview: Boolean) { |
|
|
|
private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview: Boolean) { |
|
|
|
val owner = LocalLifecycleOwner.current |
|
|
|
val owner = LocalLifecycleOwner.current |
|
|
|
var visible by remember(owner) { mutableStateOf(owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) } |
|
|
|
var visible by remember(owner) { mutableStateOf(owner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) } |
|
|
|
DisposableEffect(owner) { |
|
|
|
DisposableEffect(owner) { |
|
|
|
@ -145,12 +132,12 @@ private fun LiveStreamView(group: Long, source: Long, localPreview: Boolean) { |
|
|
|
owner.lifecycle.addObserver(observer) |
|
|
|
owner.lifecycle.addObserver(observer) |
|
|
|
onDispose { owner.lifecycle.removeObserver(observer) } |
|
|
|
onDispose { owner.lifecycle.removeObserver(observer) } |
|
|
|
} |
|
|
|
} |
|
|
|
if (visible) LiveStreamSurface(group, source, localPreview) |
|
|
|
if (visible) LiveStreamSurface(group, source, epoch, localPreview) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Composable |
|
|
|
@Composable |
|
|
|
private fun LiveStreamSurface(group: Long, source: Long, localPreview: Boolean) { |
|
|
|
private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPreview: Boolean) { |
|
|
|
val engine = remember(group, source, localPreview) { StreamPlayerEngine(group, source, localPreview) } |
|
|
|
val engine = remember(group, source, epoch, localPreview) { StreamPlayerEngine(group, source, epoch, localPreview) } |
|
|
|
val aspect by engine.aspect.collectAsState() |
|
|
|
val aspect by engine.aspect.collectAsState() |
|
|
|
val status by engine.status.collectAsState() |
|
|
|
val status by engine.status.collectAsState() |
|
|
|
var surface by remember { mutableStateOf<Surface?>(null) } |
|
|
|
var surface by remember { mutableStateOf<Surface?>(null) } |
|
|
|
|