feat: Explicit answer/reject for inbound calls (#2504)
This commit is contained in:
committed by
dermotduffy
parent
b9f09b7c4e
commit
f397596ed1
@@ -0,0 +1,11 @@
|
||||
import { CallAnswerActionConfig } from '../../../config/schema/actions/custom/call-answer';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class CallAnswerAction extends AdvancedCameraCardAction<CallAnswerActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
api.getCallManager().answer();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { ActionContext } from 'action';
|
||||
import { INTERNAL_CALLBACK_ACTION } from '../../config/schema/actions/custom/internal';
|
||||
import { ActionConfig, AuxillaryActionConfig } from '../../config/schema/actions/types';
|
||||
import { isAdvancedCameraCardCustomAction } from '../../utils/action';
|
||||
import { CallAnswerAction } from './actions/call-answer';
|
||||
import { CallEndAction } from './actions/call-end';
|
||||
import { CallServiceAction } from './actions/call-service';
|
||||
import { CallStartAction } from './actions/call-start';
|
||||
@@ -123,10 +124,12 @@ export class ActionFactory {
|
||||
return new InfoAction(context, action, options?.config);
|
||||
case 'menu_toggle':
|
||||
return new MenuToggleAction(context, action, options?.config);
|
||||
case 'call_start':
|
||||
return new CallStartAction(context, action, options?.config);
|
||||
case 'call_answer':
|
||||
return new CallAnswerAction(context, action, options?.config);
|
||||
case 'call_end':
|
||||
return new CallEndAction(context, action, options?.config);
|
||||
case 'call_start':
|
||||
return new CallStartAction(context, action, options?.config);
|
||||
case 'camera_select':
|
||||
return new CameraSelectAction(context, action, options?.config);
|
||||
case 'substream_off':
|
||||
|
||||
@@ -22,8 +22,7 @@ export class CallManager {
|
||||
public initialize(): void {
|
||||
// A call runs on the live view of a specific camera. The listener watches
|
||||
// condition state so the call can be ended when the view, camera, or
|
||||
// engaged substream moves off what the call started on -- and so an inbound
|
||||
// call can register the user's "answer" (microphone un-mute).
|
||||
// engaged substream moves off what the call started on.
|
||||
this._api.getConditionStateManager().addListener(this._handleConditionStateChange);
|
||||
}
|
||||
|
||||
@@ -119,8 +118,9 @@ export class CallManager {
|
||||
this._end(false);
|
||||
}
|
||||
|
||||
// An already-unmuted mic is treated as "answered" for an inbound call.
|
||||
const answered = inbound && !this._api.getMicrophoneManager().isMuted();
|
||||
// Outbound calls are answered by construction (the user initiated them);
|
||||
// inbound calls start unanswered and wait for an explicit Answer.
|
||||
const answered = !inbound;
|
||||
|
||||
this._call = {
|
||||
cameraID: parentID,
|
||||
@@ -173,6 +173,24 @@ export class CallManager {
|
||||
return this._end(true);
|
||||
}
|
||||
|
||||
// Marks an inbound ringing call as answered: stops the ringtone, cancels
|
||||
// the unanswered timer, and lets the normal call controls take over.
|
||||
// No-op (returns false) if there is no call or it's already answered;
|
||||
// rejecting a ringing call uses `end()` (same teardown).
|
||||
public answer(): boolean {
|
||||
if (!this._call || this._call.answered) {
|
||||
return false;
|
||||
}
|
||||
this._ringtone.stop();
|
||||
this._unansweredTimer.stop();
|
||||
// Replace (don't mutate) so Lit identity checks downstream pick up the
|
||||
// change. The `update()` below forces card.ts to re-render and re-read
|
||||
// `getCall()`, propagating the new session to the carousel.
|
||||
this._call = { ...this._call, answered: true };
|
||||
this._api.getCardElementManager().update();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ends the active call iff every supplied predicate matches the session.
|
||||
// Returns true iff a call was actually ended.
|
||||
public endIf(options: {
|
||||
@@ -268,34 +286,15 @@ export class CallManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Watches condition state for two transitions during an active call:
|
||||
//
|
||||
// 1. End the call once it can no longer be conducted from where it started
|
||||
// (e.g. view change). Only react to changes in view/camera/substream
|
||||
// themselves -- not to unrelated state updates (e.g. `mediaLoadedInfo`)
|
||||
// that may arrive before the view-manager's own state update.
|
||||
//
|
||||
// 2. Register an inbound call as "answered" the first time the microphone
|
||||
// un-mutes during the call -- a muted->unmuted transition. Idempotent:
|
||||
// once answered we never flip back, so re-muting later does not undo it.
|
||||
// Answering also silences the ringtone.
|
||||
// Ends the call once it can no longer be conducted from where it started
|
||||
// (e.g. view change). Only reacts to changes in view/camera/substream
|
||||
// themselves -- not to unrelated state updates (e.g. `mediaLoadedInfo`)
|
||||
// that may arrive before the view-manager's own state update.
|
||||
private _handleConditionStateChange = (stateChange: ConditionStateChange): void => {
|
||||
if (!this._call) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._call.inbound &&
|
||||
!this._call.answered &&
|
||||
stateChange.change.microphone &&
|
||||
stateChange.new.microphone?.muted === false &&
|
||||
stateChange.old.microphone?.muted !== false
|
||||
) {
|
||||
this._call.answered = true;
|
||||
this._ringtone.stop();
|
||||
this._unansweredTimer.stop();
|
||||
}
|
||||
|
||||
const viewRelevantChange =
|
||||
stateChange.change.view !== undefined ||
|
||||
stateChange.change.camera !== undefined ||
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface CardAutomationsAPI {
|
||||
|
||||
export interface CardCallAPI {
|
||||
getCameraManager(): CameraManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
|
||||
Reference in New Issue
Block a user