Reacquire mic after screen lock kills capture mid-call (#339)
Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social>
This commit is contained in:
parent
7dcc713ec2
commit
72ee0c9ed6
2 changed files with 51 additions and 0 deletions
|
|
@ -14,6 +14,7 @@ import {
|
||||||
supportsAudioOutputSelection,
|
supportsAudioOutputSelection,
|
||||||
type AudioCaptureOptions,
|
type AudioCaptureOptions,
|
||||||
} from "livekit-client"
|
} from "livekit-client"
|
||||||
|
import {App} from "@capacitor/app"
|
||||||
import {derived, get, writable} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {first, not, nthEq, reject, uniqBy} from "@welshman/lib"
|
import {first, not, nthEq, reject, uniqBy} from "@welshman/lib"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, sortEventsDesc, tagSpec, tagValues} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, sortEventsDesc, tagSpec, tagValues} from "@welshman/util"
|
||||||
|
|
@ -353,6 +354,25 @@ export const switchCallActiveDevice = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On mobile, locking the screen can suspend microphone capture without ever
|
||||||
|
* ending the underlying MediaStreamTrack: the local participant still looks
|
||||||
|
* connected and unmuted, but publishes silence until the track is manually
|
||||||
|
* reacquired. LiveKit only guards against this for tracks attached to a DOM
|
||||||
|
* element (i.e. video), so the mic needs the same treatment on foreground
|
||||||
|
* return. `App.addListener("appStateChange", ...)` fires from Capacitor's web
|
||||||
|
* fallback too, so this covers both native and browser tabs.
|
||||||
|
*/
|
||||||
|
export const syncCallAudioResume = () => {
|
||||||
|
const listener = App.addListener("appStateChange", ({isActive}) => {
|
||||||
|
if (isActive) void reacquireMicrophoneIfNeeded()
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
listener.then(l => l.remove())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const resetVideoCallLayout = () => {
|
export const resetVideoCallLayout = () => {
|
||||||
videoCallLayout.set(VideoCallLayout.Chat)
|
videoCallLayout.set(VideoCallLayout.Chat)
|
||||||
}
|
}
|
||||||
|
|
@ -539,6 +559,33 @@ const setUpMicrophone = async (
|
||||||
return muted
|
return muted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reacquireMicrophoneIfNeeded = async () => {
|
||||||
|
const session = get(currentCallSession)
|
||||||
|
if (!session || get(callMicMuted)) return
|
||||||
|
|
||||||
|
const track = session.livekit.localParticipant.getTrackPublication(
|
||||||
|
Track.Source.Microphone,
|
||||||
|
)?.audioTrack
|
||||||
|
if (!track || track.isMuted || track.isUserProvided) return
|
||||||
|
|
||||||
|
// Mirrors LiveKit's own (mobile-only, video-track-only) reacquisition
|
||||||
|
// check: a capture device that died silently still reports readyState
|
||||||
|
// "live", but the browser flips `muted`/`enabled` on the underlying
|
||||||
|
// MediaStreamTrack. Checking for actual silence instead would false-
|
||||||
|
// positive any time the user simply isn't talking.
|
||||||
|
const {mediaStreamTrack} = track
|
||||||
|
const needsReacquisition =
|
||||||
|
mediaStreamTrack.readyState !== "live" || mediaStreamTrack.muted || !mediaStreamTrack.enabled
|
||||||
|
if (!needsReacquisition) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await track.restartTrack()
|
||||||
|
} catch {
|
||||||
|
// Best-effort: the user can still recover via mute/unmute or by
|
||||||
|
// rejoining if reacquiring the mic fails here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const clearReconnectSchedule = () => {
|
const clearReconnectSchedule = () => {
|
||||||
if (reconnectTimeout !== undefined) {
|
if (reconnectTimeout !== undefined) {
|
||||||
clearTimeout(reconnectTimeout)
|
clearTimeout(reconnectTimeout)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
import {Push} from "@app/push"
|
import {Push} from "@app/push"
|
||||||
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
||||||
import {syncKeyboard} from "@app/keyboard"
|
import {syncKeyboard} from "@app/keyboard"
|
||||||
|
import {syncCallAudioResume} from "@app/call"
|
||||||
import {getPageTitle} from "@app/title"
|
import {getPageTitle} from "@app/title"
|
||||||
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
||||||
|
|
||||||
|
|
@ -163,6 +164,9 @@
|
||||||
// Initialize keyboard state tracking
|
// Initialize keyboard state tracking
|
||||||
unsubscribers.push(syncKeyboard())
|
unsubscribers.push(syncKeyboard())
|
||||||
|
|
||||||
|
// Reacquire the mic if the screen locked mid-call and capture died
|
||||||
|
unsubscribers.push(syncCallAudioResume())
|
||||||
|
|
||||||
// Subscribe to badge count for changes
|
// Subscribe to badge count for changes
|
||||||
unsubscribers.push(notifications.syncBadges())
|
unsubscribers.push(notifications.syncBadges())
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue