mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(voice): harden Gemini Live provider integration
This commit is contained in:
1 parent
d4492d8f8c
commit
4fde920665
102 files changed
+28528
-1581
No files matched your search
+123
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user