fix(electron): streaming audio encoding not working (#12231)

fix AF-2612

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved handling of recording status updates to prevent unwanted side effects after closing the recording popup.
  - Enhanced error handling and logging for internal communication, ensuring errors are logged and do not disrupt the user experience.
  - Refined control flow for stream encoding to avoid unnecessary errors when encoders are closed.
- **Refactor**
  - Updated popup window readiness and lifecycle management for more reliable behavior and clearer logging.
  - Adjusted recording state management to allow more precise control over status updates and emissions.
- **Style**
  - Added additional logging for popup and recording operations to aid in troubleshooting and transparency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
pengx17
2025-05-13 06:52:09 +00:00
parent 9d116426f8
commit 7e80cb336e
6 changed files with 40 additions and 18 deletions
@@ -148,6 +148,7 @@ export function Recording() {
); );
useEffect(() => { useEffect(() => {
let removed = false;
let currentStreamEncoder: OpusStreamEncoder | undefined; let currentStreamEncoder: OpusStreamEncoder | undefined;
apis?.recording apis?.recording
@@ -161,6 +162,9 @@ export function Recording() {
.catch(console.error); .catch(console.error);
const handleRecordingStatusChanged = async (status: Status) => { const handleRecordingStatusChanged = async (status: Status) => {
if (removed) {
return;
}
if (status?.status === 'new') { if (status?.status === 'new') {
track.popup.$.recordingBar.toggleRecordingBar({ track.popup.$.recordingBar.toggleRecordingBar({
type: 'Meeting record', type: 'Meeting record',
@@ -196,6 +200,7 @@ export function Recording() {
}); });
return () => { return () => {
removed = true;
unsubscribe?.(); unsubscribe?.();
currentStreamEncoder?.close(); currentStreamEncoder?.close();
}; };
@@ -80,7 +80,12 @@ export const registerHandlers = () => {
}; };
ipcMain.handle(AFFINE_API_CHANNEL_NAME, async (e, ...args: any[]) => { ipcMain.handle(AFFINE_API_CHANNEL_NAME, async (e, ...args: any[]) => {
return handleIpcMessage(e, ...args); try {
return await handleIpcMessage(e, ...args);
} catch (error) {
logger.error(`error in ipc handler when calling ${args[0]}`, error);
return null;
}
}); });
ipcMain.on(AFFINE_API_CHANNEL_NAME, (e, ...args: any[]) => { ipcMain.on(AFFINE_API_CHANNEL_NAME, (e, ...args: any[]) => {
@@ -520,10 +520,13 @@ export function newRecording(
export function startRecording( export function startRecording(
appGroup?: AppGroupInfo | number appGroup?: AppGroupInfo | number
): RecordingStatus | null { ): RecordingStatus | null {
const state = recordingStateMachine.dispatch({ const state = recordingStateMachine.dispatch(
type: 'START_RECORDING', {
appGroup: normalizeAppGroupInfo(appGroup), type: 'START_RECORDING',
}); appGroup: normalizeAppGroupInfo(appGroup),
},
false
);
if (state?.status === 'recording') { if (state?.status === 'recording') {
createRecording(state); createRecording(state);
@@ -542,6 +545,8 @@ export function startRecording(
} }
}, MAX_DURATION_FOR_TRANSCRIPTION); }, MAX_DURATION_FOR_TRANSCRIPTION);
recordingStateMachine.status$.next(state);
return state; return state;
} }
@@ -654,6 +659,8 @@ export async function getRawAudioBuffers(
} }
export async function readyRecording(id: number, buffer: Buffer) { export async function readyRecording(id: number, buffer: Buffer) {
logger.info('readyRecording', id);
const recordingStatus = recordingStatus$.value; const recordingStatus = recordingStatus$.value;
const recording = recordings.get(id); const recording = recordings.get(id);
if (!recordingStatus || recordingStatus.id !== id || !recording) { if (!recordingStatus || recordingStatus.id !== id || !recording) {
@@ -63,7 +63,7 @@ export class RecordingStateMachine {
* @param event The event to dispatch * @param event The event to dispatch
* @returns The new recording status after the event is processed * @returns The new recording status after the event is processed
*/ */
dispatch(event: RecordingEvent): RecordingStatus | null { dispatch(event: RecordingEvent, emit = true): RecordingStatus | null {
const currentStatus = this.recordingStatus$.value; const currentStatus = this.recordingStatus$.value;
let newStatus: RecordingStatus | null = null; let newStatus: RecordingStatus | null = null;
@@ -105,7 +105,9 @@ export class RecordingStateMachine {
return currentStatus; return currentStatus;
} }
this.recordingStatus$.next(newStatus); if (emit) {
this.recordingStatus$.next(newStatus);
}
return newStatus; return newStatus;
} }
@@ -61,10 +61,7 @@ abstract class PopupWindow {
abstract windowOptions: Partial<BrowserWindowConstructorOptions>; abstract windowOptions: Partial<BrowserWindowConstructorOptions>;
resolveReady: () => void = () => {}; ready = Promise.withResolvers<void>();
ready = new Promise<void>(resolve => {
this.resolveReady = resolve;
});
private readonly showing$ = new BehaviorSubject<boolean>(false); private readonly showing$ = new BehaviorSubject<boolean>(false);
@@ -109,12 +106,12 @@ abstract class PopupWindow {
visibleOnFullScreen: true, visibleOnFullScreen: true,
}); });
browserWindow.loadURL(popupViewUrl).catch(err => logger.error(err)); logger.info('loading popup', this.name, popupViewUrl);
browserWindow.on('ready-to-show', () => { browserWindow.webContents.on('did-finish-load', () => {
browserWindow.webContents.on('did-finish-load', () => { this.ready.resolve();
this.resolveReady(); logger.info('popup ready', this.name);
});
}); });
browserWindow.loadURL(popupViewUrl).catch(err => logger.error(err));
return browserWindow; return browserWindow;
} }
@@ -126,7 +123,7 @@ abstract class PopupWindow {
const workArea = getCurrentDisplay(browserWindow).workArea; const workArea = getCurrentDisplay(browserWindow).workArea;
const popupSize = browserWindow.getSize(); const popupSize = browserWindow.getSize();
await this.ready; await this.ready.promise;
this.showing$.next(true); this.showing$.next(true);
@@ -141,6 +138,8 @@ abstract class PopupWindow {
// Set initial position // Set initial position
browserWindow.setPosition(startX, y); browserWindow.setPosition(startX, y);
logger.info('showing popup', this.name);
// First fade in, then slide // First fade in, then slide
await Promise.all([ await Promise.all([
// Slide in animation // Slide in animation
@@ -169,6 +168,7 @@ abstract class PopupWindow {
if (!this.browserWindow) { if (!this.browserWindow) {
return; return;
} }
logger.info('hiding popup', this.name);
this.showing$.next(false); this.showing$.next(false);
await animate(this.browserWindow.getOpacity(), 0, opacity => { await animate(this.browserWindow.getOpacity(), 0, opacity => {
this.browserWindow?.setOpacity(opacity); this.browserWindow?.setOpacity(opacity);
@@ -423,9 +423,12 @@ export const createStreamEncoder = (
let isClosed = false; let isClosed = false;
const next = async () => { const next = async () => {
if (!apis || isClosed) { if (!apis) {
throw new Error('Electron API is not available'); throw new Error('Electron API is not available');
} }
if (isClosed) {
return;
}
const { buffer, nextCursor } = await apis.recording.getRawAudioBuffers( const { buffer, nextCursor } = await apis.recording.getRawAudioBuffers(
recordingId, recordingId,
cursor cursor