Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ class WebRtcDebugFragment : Fragment() {
HomeAssistantAppTheme {
val session by viewModel.session.collectAsStateWithLifecycle()
val playerState by viewModel.playerState.collectAsStateWithLifecycle()
val micState by viewModel.micState.collectAsStateWithLifecycle()
WebRtcDebugView(
player = session,
playerState = playerState,
micState = micState,
eglContext = viewModel.eglContext,
onStart = viewModel::startSession,
onStop = viewModel::stopSession,
onMicEnabled = viewModel::setMicEnabled,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.webrtc.core.MicState
import io.homeassistant.companion.android.webrtc.core.PlayerState
import io.homeassistant.companion.android.webrtc.core.audio.AudioController
import io.homeassistant.companion.android.webrtc.core.session.WebRtcSession
import io.homeassistant.companion.android.webrtc.core.session.libwebrtc.LibWebRtcPeerConnectionControllerFactory
import io.homeassistant.companion.android.webrtc.signaling.HaSignalingClient
Expand All @@ -31,6 +33,7 @@ private val STATE_SHARING_TIMEOUT = 5.seconds
class WebRtcDebugViewModel @Inject constructor(
private val serverManager: ServerManager,
private val controllerFactory: LibWebRtcPeerConnectionControllerFactory,
private val audioController: AudioController,
) : ViewModel() {

private val _session = MutableStateFlow<WebRtcSession?>(null)
Expand All @@ -45,6 +48,15 @@ class WebRtcDebugViewModel @Inject constructor(
initialValue = PlayerState.Idle,
)

@OptIn(ExperimentalCoroutinesApi::class)
val micState: StateFlow<MicState> = _session
.flatMapLatest { session -> session?.micState ?: flowOf(MicState.Off) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(STATE_SHARING_TIMEOUT.inWholeMilliseconds),
initialValue = MicState.Off,
)

/** EGL context the renderer must share with the hardware decoder. */
val eglContext: EglBase.Context
get() = controllerFactory.eglBase.eglBaseContext
Expand All @@ -59,10 +71,16 @@ class WebRtcDebugViewModel @Inject constructor(
entityId = trimmedEntityId,
signalingClient = signalingClient,
controllerFactory = controllerFactory,
audioController = audioController,
).also { it.start() }
}
}

/** The caller must hold the `RECORD_AUDIO` permission before enabling the microphone. */
fun setMicEnabled(enabled: Boolean) {
_session.value?.setMicEnabled(enabled)
}

fun stopSession() {
_session.value?.release()
_session.value = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package io.homeassistant.companion.android.settings.developer.webrtc.views

import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -14,18 +20,23 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.webrtc.compose.WebRtcVideo
import io.homeassistant.companion.android.webrtc.core.CameraPlayer
import io.homeassistant.companion.android.webrtc.core.MicState
import io.homeassistant.companion.android.webrtc.core.PlayerFailure
import io.homeassistant.companion.android.webrtc.core.PlayerState
import livekit.org.webrtc.EglBase
Expand All @@ -40,9 +51,11 @@ private const val VIDEO_ASPECT_RATIO = 16f / 9f
fun WebRtcDebugView(
player: CameraPlayer?,
playerState: PlayerState,
micState: MicState,
eglContext: EglBase.Context?,
onStart: (String) -> Unit,
onStop: () -> Unit,
onMicEnabled: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
var entityId by rememberSaveable { mutableStateOf("") }
Expand All @@ -68,10 +81,14 @@ fun WebRtcDebugView(
Button(onClick = onStop, enabled = player != null) {
Text(stringResource(commonR.string.webrtc_debug_stop))
}
PushToTalkButton(
enabled = player != null,
onMicEnabled = onMicEnabled,
)
}
// Raw technical state, on purpose not localized on this developer-only screen
Text(
text = playerState.toDebugLabel(),
text = "${playerState.toDebugLabel()} | mic: ${micState.toDebugLabel()}",
style = MaterialTheme.typography.bodyMedium,
)
player?.let {
Expand All @@ -94,6 +111,44 @@ fun WebRtcDebugView(
}
}

/**
* Push-to-talk: the microphone is live only while the button is pressed. The first press requests
* the `RECORD_AUDIO` permission instead of enabling the microphone.
*/
@Composable
private fun PushToTalkButton(enabled: Boolean, onMicEnabled: (Boolean) -> Unit) {
val context = LocalContext.current
var hasMicPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED,
)
}
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
hasMicPermission = granted
}

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
LaunchedEffect(isPressed, hasMicPermission, enabled) {
onMicEnabled(isPressed && hasMicPermission && enabled)
}

Button(
onClick = {
if (!hasMicPermission) {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
},
enabled = enabled,
interactionSource = interactionSource,
) {
Text(stringResource(commonR.string.webrtc_debug_talk))
}
}

private fun MicState.toDebugLabel(): String = this::class.simpleName.orEmpty()

private fun PlayerState.toDebugLabel(): String = when (this) {
is PlayerState.Failed -> "${this::class.simpleName}: ${failure.toDebugLabel()}"
else -> this::class.simpleName.orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.homeassistant.companion.android.webrtc.core.audio.AndroidAudioController
import io.homeassistant.companion.android.webrtc.core.audio.AudioController
import io.homeassistant.companion.android.webrtc.core.session.PeerConnectionController
import io.homeassistant.companion.android.webrtc.core.session.libwebrtc.LibWebRtcPeerConnectionControllerFactory
import javax.inject.Singleton
Expand All @@ -29,4 +31,12 @@ object WebRtcModule {
fun providePeerConnectionControllerFactoryInterface(
factory: LibWebRtcPeerConnectionControllerFactory,
): PeerConnectionController.Factory = factory

/**
* A single controller for the whole process: it reference counts the communication audio mode
* across all sessions, so it must be shared to balance correctly.
*/
@Provides
@Singleton
fun provideAudioController(@ApplicationContext context: Context): AudioController = AndroidAudioController(context)
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ import io.homeassistant.companion.android.util.sensitive
import io.homeassistant.companion.android.util.toRelativeUrl
import io.homeassistant.companion.android.webrtc.core.PlayerFailure
import io.homeassistant.companion.android.webrtc.core.PlayerState
import io.homeassistant.companion.android.webrtc.core.audio.AudioController
import io.homeassistant.companion.android.webrtc.core.session.WebRtcSession
import io.homeassistant.companion.android.webrtc.core.session.libwebrtc.LibWebRtcPeerConnectionControllerFactory
import io.homeassistant.companion.android.webrtc.signaling.HaSignalingClient
Expand Down Expand Up @@ -300,6 +301,9 @@ class WebViewActivity :
@Inject
lateinit var peerConnectionControllerFactory: LibWebRtcPeerConnectionControllerFactory

@Inject
lateinit var webRtcAudioController: AudioController

private lateinit var webView: WebView
private var loadedUrl: Uri? = null
private lateinit var decor: FrameLayout
Expand Down Expand Up @@ -1574,6 +1578,7 @@ class WebViewActivity :
entityId = entityId,
signalingClient = signalingClient,
controllerFactory = peerConnectionControllerFactory,
audioController = webRtcAudioController,
)
session.setAudioEnabled(!muted)
session.start()
Expand Down
1 change: 1 addition & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,7 @@
<string name="webrtc_debug_start">Start</string>
<string name="webrtc_debug_stop">Stop</string>
<string name="webrtc_debug_summary">Test the native WebRTC player against a camera entity of the active server</string>
<string name="webrtc_debug_talk">Hold to talk</string>
<string name="webrtc_native_player">Native WebRTC player (beta)</string>
<string name="webrtc_native_player_summary">Let dashboards play camera WebRTC streams with the native player instead of the WebView</string>
<string name="websocket_listening">Connected to Home Assistant</string>
Expand Down
1 change: 1 addition & 0 deletions webrtc-compose/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ androidx.lifecycle:lifecycle-viewmodel-savedstate:2.11.0=debugAndroidTestCompile
androidx.lifecycle:lifecycle-viewmodel:2.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestCompileClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestCompileClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestCompileClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestCompileClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.media:media:1.8.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.print:print:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
androidx.savedstate:savedstate-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugScreenshotTestCompileClasspath,debugScreenshotTestLintChecksClasspath,debugScreenshotTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath,releaseScreenshotTestCompileClasspath,releaseScreenshotTestLintChecksClasspath,releaseScreenshotTestRuntimeClasspath
Expand Down
4 changes: 4 additions & 0 deletions webrtc-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)

// AudioFocusRequestCompat, the audio focus API compatible with the min SDK
implementation(libs.androidx.media)

// This module is the only one allowed to depend on libwebrtc directly so the artifact can be
// swapped without touching consumers. `api` because VideoSink is part of the public player
// interfaces used by renderers.
api(libs.webrtc.sdk)

testImplementation(libs.junit.jupiter.params)
testImplementation(libs.androidx.test.core)
}
8 changes: 5 additions & 3 deletions webrtc-core/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ androidx.compose.ui:ui-util-android:1.11.4=debugUnitTestLintChecksClasspath,debu
androidx.compose.ui:ui-util:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose.ui:ui:1.11.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.compose:compose-bom:2026.06.01=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures-ktx:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.concurrent:concurrent-futures:1.2.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.core:core-ktx:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.core:core:1.19.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
Expand Down Expand Up @@ -83,6 +85,7 @@ androidx.lifecycle:lifecycle-viewmodel:2.6.2=debugAndroidTestCompileClasspath,de
androidx.lifecycle:lifecycle-viewmodel:2.9.4=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.loader:loader:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.localbroadcastmanager:localbroadcastmanager:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.media:media:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.print:print:1.0.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.3.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.profileinstaller:profileinstaller:1.4.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
Expand All @@ -98,8 +101,7 @@ androidx.test.espresso:espresso-idling-resource:3.7.0=debugUnitTestLintChecksCla
androidx.test.ext:junit:1.1.5=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test.services:storage:1.4.2=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test:annotation:1.0.1=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test:core:1.4.0=debugUnitTestCompileClasspath
androidx.test:core:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test:core:1.7.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test:monitor:1.8.0=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.test:runner:1.5.0=debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
androidx.tracing:tracing:1.1.0=debugUnitTestCompileClasspath
Expand Down
Loading
Loading