|
|
|
|
@ -8,9 +8,13 @@ import com.utun.chat.MainActivity
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope |
|
|
|
|
import kotlinx.coroutines.Dispatchers |
|
|
|
|
import kotlinx.coroutines.SupervisorJob |
|
|
|
|
import kotlinx.coroutines.Job |
|
|
|
|
import kotlinx.coroutines.delay |
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow |
|
|
|
|
import kotlinx.coroutines.flow.StateFlow |
|
|
|
|
import kotlinx.coroutines.flow.combine |
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChanged |
|
|
|
|
import kotlinx.coroutines.flow.collect |
|
|
|
|
import kotlinx.coroutines.launch |
|
|
|
|
import java.nio.ByteBuffer |
|
|
|
|
import java.nio.ByteOrder |
|
|
|
|
@ -26,6 +30,10 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
val state: StateFlow<CallState?> = _state |
|
|
|
|
private val _video = MutableStateFlow<CallVideoEngine?>(null) |
|
|
|
|
val video: StateFlow<CallVideoEngine?> = _video |
|
|
|
|
private val _videoActive = MutableStateFlow(false) |
|
|
|
|
val videoActive: StateFlow<Boolean> = _videoActive |
|
|
|
|
private var videoJob: Job? = null |
|
|
|
|
private var videoSpeakerPending = false |
|
|
|
|
fun setVideo(settings: CallVideoSettings) { |
|
|
|
|
checkMain() |
|
|
|
|
val owner = service ?: return |
|
|
|
|
@ -41,7 +49,7 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
private val _earpiece = MutableStateFlow(true) |
|
|
|
|
val earpiece: StateFlow<Boolean> = _earpiece |
|
|
|
|
private val proximity = ProximityManager(ChatApplication.instance) |
|
|
|
|
val proximityNear: StateFlow<Boolean> = proximity.near |
|
|
|
|
val proximityNear: StateFlow<Boolean?> = proximity.near |
|
|
|
|
|
|
|
|
|
private var service: CallService? = null |
|
|
|
|
|
|
|
|
|
@ -60,6 +68,7 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
if (initialized) return |
|
|
|
|
initialized = true |
|
|
|
|
TelecomCallManager.setListener(this) |
|
|
|
|
scope.launch { proximity.near.collect { applyVideoSpeakerIfReady() } } |
|
|
|
|
scope.launch { |
|
|
|
|
while (true) { |
|
|
|
|
delay(500) |
|
|
|
|
@ -209,6 +218,7 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
accepting = false |
|
|
|
|
acceptSent = false |
|
|
|
|
answerWhenVisible = 0L |
|
|
|
|
videoJob?.cancel(); videoJob = null; videoSpeakerPending = false; _videoActive.value = false |
|
|
|
|
owner?.shutdown() |
|
|
|
|
_video.value = null |
|
|
|
|
proximity.stop() |
|
|
|
|
@ -256,7 +266,9 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
|
|
|
|
|
fun setRoute(route: AudioRoute) { |
|
|
|
|
checkMain() |
|
|
|
|
videoSpeakerPending = false // Явный выбор пользователя отменяет ожидающее автоматическое переключение. |
|
|
|
|
service?.audio?.router?.setRoute(route) |
|
|
|
|
syncProximity() |
|
|
|
|
_state.value?.let { TelecomCallManager.setRoute(it.callId, route) } |
|
|
|
|
scope.launch { ChatApplication.instance.configProvider.setCallAudioRoute(route) } |
|
|
|
|
} |
|
|
|
|
@ -264,10 +276,30 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
private fun routeChanged(route: AudioRoute, headset: Boolean) { |
|
|
|
|
_route.value = route |
|
|
|
|
_headset.value = headset |
|
|
|
|
if (route == AudioRoute.EARPIECE && _state.value?.phase == CallPhase.ACTIVE) proximity.start() else proximity.stop() |
|
|
|
|
if (route != AudioRoute.EARPIECE) videoSpeakerPending = false |
|
|
|
|
syncProximity() |
|
|
|
|
service?.audio?.syncAecForRoute(route) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun syncProximity() { |
|
|
|
|
if (_state.value?.phase == CallPhase.ACTIVE && _route.value == AudioRoute.EARPIECE |
|
|
|
|
&& (!_videoActive.value || videoSpeakerPending)) proximity.start() else proximity.stop() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun applyVideoSpeakerIfReady() { |
|
|
|
|
if (!videoSpeakerPending || !_videoActive.value || proximity.near.value != false || _route.value != AudioRoute.EARPIECE) return |
|
|
|
|
val st = _state.value ?: return |
|
|
|
|
val audio = service?.audio ?: return |
|
|
|
|
if (st.phase != CallPhase.ACTIVE) return |
|
|
|
|
videoSpeakerPending = false |
|
|
|
|
// Сохранённый маршрут аудиозвонков не меняем; автоследование за гарнитурой сохраняется. |
|
|
|
|
audio.router.fallbackRoute = AudioRoute.SPEAKER |
|
|
|
|
audio.router.setRoute(AudioRoute.SPEAKER, automatic = true) |
|
|
|
|
TelecomCallManager.setRoute(st.callId, AudioRoute.SPEAKER) |
|
|
|
|
syncProximity() |
|
|
|
|
LogManager.addLog("INFO", "call", "video speaker enabled after confirmed FAR id=${st.callId}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** Только живые события ядра, сериализованные AppEventHandler на main thread. */ |
|
|
|
|
fun onEvent(type: Int, data: ByteArray?) { |
|
|
|
|
checkMain() |
|
|
|
|
@ -342,6 +374,19 @@ object CallController : TelecomCallManager.Listener {
|
|
|
|
|
owner.audio.router.onChanged = { route, headset -> routeChanged(route, headset) } |
|
|
|
|
_earpiece.value = owner.audio.router.hasEarpiece |
|
|
|
|
routeChanged(owner.audio.router.route, owner.audio.router.headsetAvailable) |
|
|
|
|
owner.video?.let { engine -> |
|
|
|
|
videoJob = scope.launch { |
|
|
|
|
combine(engine.state, engine.display) { video, display -> video.settings.enabled || display != null } |
|
|
|
|
.distinctUntilChanged().collect { active -> |
|
|
|
|
if (service !== owner || _state.value?.callId != id || _state.value?.phase != CallPhase.ACTIVE) return@collect |
|
|
|
|
if (active == _videoActive.value) return@collect |
|
|
|
|
_videoActive.value = active |
|
|
|
|
videoSpeakerPending = active && _route.value == AudioRoute.EARPIECE |
|
|
|
|
LogManager.addLog("INFO", "call", "video active=$active speaker_pending=$videoSpeakerPending proximity=${proximity.near.value} id=$id") |
|
|
|
|
syncProximity(); applyVideoSpeakerIfReady() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
owner.updateNotification() |
|
|
|
|
LogManager.addLog("INFO", "Call", "active id=$id; service owns audio and CPU wake lock") |
|
|
|
|
} |
|
|
|
|
|