feat: Explicit answer/reject for inbound calls (#2504)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent b9f09b7c4e
commit f397596ed1
26 changed files with 503 additions and 362 deletions
@@ -26,7 +26,7 @@ interface MicrophoneActionsControllerOptions {
export class MicrophoneActionsController {
private _options: MicrophoneActionsControllerOptions | null = null;
private _selectedCamera: string | null = null;
private _callActive = false;
private _callAnswered = false;
private _visibilityObserver: VisibilityObserver;
constructor() {
@@ -40,20 +40,22 @@ export class MicrophoneActionsController {
}
/**
* Notifies the controller of the call-active state, acting only on a genuine
* transition. The initial state is treated as inactive, so a first-ever
* `true` counts -- the call rules apply even when the live view first
* appears during an active call.
* Notifies the controller of the call-answered state (outbound calls are
* answered at start; inbound calls only become answered when the user
* accepts). Acts only on a genuine transition. The initial state is treated
* as unanswered, so a first-ever `true` counts -- the call rules apply even
* when the live view first appears during an answered call.
*
* Call start unmutes the microphone only if the user opted into
* `microphone.auto_unmute: ['call']`.
* Call answer unmutes the microphone only if the user opted into
* `microphone.auto_unmute: ['call']`; the symmetric mute fires on the
* answered-to-unanswered transition (call end after answer).
*/
public setCallActive(active: boolean): void {
if (active === this._callActive) {
public setCallAnswered(answered: boolean): void {
if (answered === this._callAnswered) {
return;
}
this._callActive = active;
if (active) {
this._callAnswered = answered;
if (answered) {
this._unmuteIfConfigured('call');
} else {
this._muteIfConfigured('call');
+12 -10
View File
@@ -42,7 +42,7 @@ export class MediaActionsController {
// Audio-related state fed in via dedicated setters (not `setOptions`, which
// is pure configuration).
private _microphoneState?: MicrophoneState;
private _callActive = false;
private _callAnswered = false;
// Deferred because the media player is not always ready when a call starts:
// the call may start from another view, or engage a substream that is still
@@ -71,17 +71,19 @@ export class MediaActionsController {
this._microphoneStateChangeHandler(previous, state);
}
// Audio-out auto-mute/unmute driven by the call lifecycle: unmute on call
// start (hear the caller), mute on call end. Acts only on a genuine
// transition. The first-ever `true` counts as a transition: a carousel that
// loads while a call is already active (e.g. `call_start` dispatched from a
// non-live view) must still unmute.
public setCallActive(active: boolean): void {
if (active === this._callActive) {
// Audio-out auto-mute/unmute driven by call answer: unmute when the call
// is answered (hear the caller), mute when an answered call ends. Acts only
// on a genuine transition. The first-ever `true` counts as a transition: a
// carousel that loads while an answered call is already active must still
// unmute. An inbound call that's rejected pre-answer never sees a `true`,
// so neither side fires -- the camera audio is never auto-disturbed by a
// call the user didn't accept.
public setCallAnswered(answered: boolean): void {
if (answered === this._callAnswered) {
return;
}
this._callActive = active;
if (active) {
this._callAnswered = answered;
if (answered) {
this._pendingCallStartAction = true;
this._applyPendingCallStartAction();
} else {