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,
|
||||
type AudioCaptureOptions,
|
||||
} from "livekit-client"
|
||||
import {App} from "@capacitor/app"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {first, not, nthEq, reject, uniqBy} from "@welshman/lib"
|
||||
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 = () => {
|
||||
videoCallLayout.set(VideoCallLayout.Chat)
|
||||
}
|
||||
|
|
@ -539,6 +559,33 @@ const setUpMicrophone = async (
|
|||
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 = () => {
|
||||
if (reconnectTimeout !== undefined) {
|
||||
clearTimeout(reconnectTimeout)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
import {Push} from "@app/push"
|
||||
import {onPushNotificationAction, pushState} from "@app/push/adapters/common"
|
||||
import {syncKeyboard} from "@app/keyboard"
|
||||
import {syncCallAudioResume} from "@app/call"
|
||||
import {getPageTitle} from "@app/title"
|
||||
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
||||
|
||||
|
|
@ -163,6 +164,9 @@
|
|||
// Initialize keyboard state tracking
|
||||
unsubscribers.push(syncKeyboard())
|
||||
|
||||
// Reacquire the mic if the screen locked mid-call and capture died
|
||||
unsubscribers.push(syncCallAudioResume())
|
||||
|
||||
// Subscribe to badge count for changes
|
||||
unsubscribers.push(notifications.syncBadges())
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue