feat(voice): harden Gemini Live provider integration

This commit is contained in:
manjunath committed 2026-10-16 17:09:03 +05:30
1 parent d4492d8f8c
commit 4fde920665
102 files changed
+28528 -1581

No files matched your search

+123 -10
View File
@@ -36,7 +36,11 @@ export class RealtimeRadio {
this.radioVisibilityOffPending = false;
this.radioToolHandoffReservations = new Map();
this.radioHandoffDeferredByReservation = false;
this.radioVoiceResume = null;
this.radioVoiceResumeEpoch = 0;
this.radioTuningPause = null;
this.radioControlUnsubscribe = null;
this.radioStateUnsubscribe = null;
this.radioVisibilityRequestUnsubscribe = null;
this.radioVisibilityUnsubscribe = null;
}
@@ -66,17 +70,46 @@ export class RealtimeRadio {
event.origin === 'user' &&
(event.action === 'pause' || event.action === 'stop')
) {
// Tuner drag pauses the stream internally while Radio still owns the
// speaker. Its cancellation/commit settles through public state.
if (
event.action === 'pause' &&
this.radioLayer?.getUIState?.()?.tuningActive
) {
this.radioTuningPause = { attemptId: event.attemptId };
return;
}
this.cancelRadioHandoff();
} else if (
event.origin === 'user' &&
event.action === 'play' &&
this.isActive()
) {
// Explicit user playback has already reached `playing` under the voice
// hard mute. Hand the speaker to Radio without tearing its stream down.
this.stop({ preserveRadioPlayback: true });
this.resumeVoiceAfterRadio(event.action, event.attemptId);
} else if (event.origin === 'user' && event.action === 'play-request') {
// A Radio-only replacement keeps the voice it interrupted. Bind the
// right to resume to the new stream before any delayed old callbacks.
this.radioTuningPause = null;
this.radioVoiceResumeEpoch++;
if (this.radioVoiceResume)
this.radioVoiceResume = {
...this.radioVoiceResume,
attemptId: event.attemptId,
};
} else if (event.origin === 'user' && event.action === 'play') {
// The public player emits play only after the new stream succeeds.
// A failed Audio.play() must leave active voice alone.
this.suspendVoiceForRadio(event.attemptId);
}
}) || null;
let wasTuning = false;
this.radioStateUnsubscribe =
this.radioLayer?.subscribe?.((state) => {
const finishedTuning = wasTuning && state?.tuningActive === false;
wasTuning = Boolean(state?.tuningActive);
if (!finishedTuning || state.tuningAwaitingStationId) return;
// Cancel, release without a station or an invalid commit all end the
// drag paused with no new stream: the interrupted voice may return.
const tuningPause = this.radioTuningPause;
this.radioTuningPause = null;
if (tuningPause && state.audioState === 'paused')
this.resumeVoiceAfterRadio('tuning-ended', tuningPause.attemptId);
}) || null;
this.radioVisibilityRequestUnsubscribe =
this.dataManager?.subscribeVisibilityRequests?.((change) => {
if (
@@ -89,8 +122,30 @@ export class RealtimeRadio {
}) || null;
}
/** Remember voice only when confirmed Radio playback actually suspends it. */
suspendVoiceForRadio(attemptId) {
if (!this.isActive()) {
// No voice to suspend, but a voice handoff hard-muted Radio before it
// started. Confirmed playback must still become audible.
this.radioVoiceDucked = false;
this.radioLayer?.setVoiceDucked?.(false);
return;
}
const settings = this.readVoiceResumeSettings?.() || {};
const resumeEpoch = this.radioVoiceResumeEpoch;
this.stop({ preserveRadioPlayback: true });
// Tell the host why voice went quiet; Radio playback caused this stop.
this.notifyVoiceSuspended?.({ reason: 'radio' });
// Synchronous explicit OFF or provider retirement defeats this suspension.
if (resumeEpoch !== this.radioVoiceResumeEpoch) return;
this.radioVoiceResumeEpoch++;
this.radioVoiceResume = { settings, attemptId };
}
/** Pause Radio for explicit voice ownership; never resumes it automatically. */
pauseRadioForVoice() {
this.radioVoiceResume = null;
this.radioVoiceResumeEpoch++;
return silenceRadioForVoice({
duckRadio: () => this.setRadioVoiceDucking(true),
pauseRadio: () => this.radioLayer?.pause?.({ origin: 'voice-duck' }),
@@ -124,6 +179,7 @@ export class RealtimeRadio {
if (this.dataManager?.isEnabled?.('radio') === false) {
this.radioHandoffDeferredByReservation = false;
this.cancelRadioHandoff({ abortRadioSiblings: true });
this.resumeVoiceAfterRadio('disabled');
return;
}
this.resumeDeferredRadioHandoffIfUnreserved();
@@ -231,7 +287,7 @@ export class RealtimeRadio {
{
prepareRadio: () =>
this.radioLayer?.playForVoice?.({ attemptId: handoffAttemptId }),
stopVoice: () => this.stop({ preserveRadioPlayback: true }),
stopVoice: () => this.suspendVoiceForRadio(handoffAttemptId),
cancelRadio: () =>
this.radioLayer?.stopPlayback?.({
origin: 'voice-cleanup',
@@ -239,6 +295,7 @@ export class RealtimeRadio {
}),
isCurrent: () =>
this.radioHandoffInFlight &&
this.radioHandoffAttemptId === handoffAttemptId &&
!this.isRadioHandoffReserved() &&
handoffEpoch === this.radioHandoffEpoch &&
!this.userTurnPending &&
@@ -246,7 +303,9 @@ export class RealtimeRadio {
handoffChannel?.readyState === 'open',
},
);
const stillCurrent = handoffEpoch === this.radioHandoffEpoch;
const stillCurrent =
handoffEpoch === this.radioHandoffEpoch &&
this.radioHandoffAttemptId === handoffAttemptId;
if (this.radioHandoffAttemptId === handoffAttemptId) {
this.radioHandoffInFlight = false;
this.radioHandoffAttemptId = null;
@@ -265,6 +324,25 @@ export class RealtimeRadio {
}
}
/** Revoke one call's prepared/preflight result, preserving other owners. */
cancelPlaybackResult(result) {
if (!result) return;
if (this.pendingRadioPlaybackResult === result) {
this.pendingRadioPlaybackResult = null;
this.radioHandoffDeferredByReservation = false;
}
if (this.radioHandoffInFlightResult !== result) return;
const attemptId = this.radioHandoffAttemptId;
const shouldStopPlayback = this.radioHandoffInFlight;
this.radioHandoffInFlightResult = null;
this.radioHandoffInFlight = false;
this.radioHandoffAttemptId = null;
// Do not advance the group epoch or release another tool's reservation.
// Late preflight completion remains bound to its own attempt ID.
if (shouldStopPlayback)
this.radioLayer?.stopPlayback?.({ origin: 'voice-cleanup', attemptId });
}
/** Invalidate delayed Radio work inside the requested authority scope. */
abortRadioSiblingTools({ responseId = null, scope = 'all' } = {}) {
for (const [controller, metadata] of this.activeRadioToolControllers) {
@@ -311,6 +389,10 @@ export class RealtimeRadio {
this.radioHandoffEpoch++;
}
stopHandoff({ preserveRadioPlayback = false } = {}) {
if (!preserveRadioPlayback) {
this.radioVoiceResume = null;
this.radioVoiceResumeEpoch++;
}
const radioHandoffAttemptId = this.radioHandoffAttemptId;
if (this.radioHandoffInFlight && !preserveRadioPlayback) {
this.radioLayer?.stopPlayback?.({
@@ -332,10 +414,16 @@ export class RealtimeRadio {
}
detachObservers() {
this.radioVoiceResume = null;
this.radioVoiceResumeEpoch++;
if (this.radioControlUnsubscribe) {
this.radioControlUnsubscribe();
this.radioControlUnsubscribe = null;
}
if (this.radioStateUnsubscribe) {
this.radioStateUnsubscribe();
this.radioStateUnsubscribe = null;
}
if (this.radioVisibilityRequestUnsubscribe) {
this.radioVisibilityRequestUnsubscribe();
this.radioVisibilityRequestUnsubscribe = null;
@@ -361,4 +449,29 @@ export class RealtimeRadio {
deferHandoff() {
this.radioHandoffDeferredByReservation = true;
}
/** Resume once after the same voice-owned Radio handoff is paused or disabled. */
resumeVoiceAfterRadio(reason, attemptId) {
const lease = this.radioVoiceResume;
if (!lease) return false;
if (attemptId !== undefined && attemptId !== lease.attemptId) return false;
const resumeEpoch = ++this.radioVoiceResumeEpoch;
void Promise.resolve()
.then(() => {
if (
resumeEpoch !== this.radioVoiceResumeEpoch ||
this.radioVoiceResume !== lease
)
return;
this.radioVoiceResume = null;
return this.resumeVoice?.(lease.settings);
})
.catch((error) =>
this.debugLog?.('radio.voice_resume_failed', {
reason,
error: error?.message || String(error),
}),
);
return true;
}
}