mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 17:46:18 +08:00
feat(native): record encoding (#14188)
fix #13784 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Start/stop system or meeting recordings with Ogg/Opus artifacts and native start/stop APIs; workspace backup recovery. * **Refactor** * Simplified recording lifecycle and UI flows; native runtime now orchestrates recording/processing and reporting. * **Bug Fixes** * Stronger path validation, safer import/export dialogs, consistent error handling/logging, and retry-safe recording processing. * **Chores** * Added cross-platform native audio capture and Ogg/Opus encoding support. * **Tests** * New unit, integration, and e2e tests for recording, path guards, dialogs, and workspace recovery. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -414,98 +414,3 @@ export async function encodeAudioBlobToOpusSlices(
|
||||
await audioContext.close();
|
||||
}
|
||||
}
|
||||
|
||||
export const createStreamEncoder = (
|
||||
recordingId: number,
|
||||
codecs: {
|
||||
sampleRate: number;
|
||||
numberOfChannels: number;
|
||||
targetBitrate?: number;
|
||||
}
|
||||
) => {
|
||||
const { encoder, encodedChunks } = createOpusEncoder({
|
||||
sampleRate: codecs.sampleRate,
|
||||
numberOfChannels: codecs.numberOfChannels,
|
||||
bitrate: codecs.targetBitrate,
|
||||
});
|
||||
|
||||
const toAudioData = (buffer: Uint8Array) => {
|
||||
// Each sample in f32 format is 4 bytes
|
||||
const BYTES_PER_SAMPLE = 4;
|
||||
return new AudioData({
|
||||
format: 'f32',
|
||||
sampleRate: codecs.sampleRate,
|
||||
numberOfChannels: codecs.numberOfChannels,
|
||||
numberOfFrames:
|
||||
buffer.length / BYTES_PER_SAMPLE / codecs.numberOfChannels,
|
||||
timestamp: 0,
|
||||
data: toArrayBuffer(buffer),
|
||||
});
|
||||
};
|
||||
|
||||
let cursor = 0;
|
||||
let isClosed = false;
|
||||
|
||||
const next = async () => {
|
||||
if (!apis) {
|
||||
throw new Error('Electron API is not available');
|
||||
}
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
const { buffer, nextCursor } = await apis.recording.getRawAudioBuffers(
|
||||
recordingId,
|
||||
cursor
|
||||
);
|
||||
if (isClosed || cursor === nextCursor) {
|
||||
return;
|
||||
}
|
||||
cursor = nextCursor;
|
||||
logger.debug('Encoding next chunk', cursor, nextCursor);
|
||||
encoder.encode(toAudioData(buffer));
|
||||
};
|
||||
|
||||
const poll = async () => {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
logger.debug('Polling next chunk');
|
||||
await next();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await poll();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
isClosed = true;
|
||||
return encoder.close();
|
||||
};
|
||||
|
||||
return {
|
||||
id: recordingId,
|
||||
next,
|
||||
poll,
|
||||
flush: () => {
|
||||
return encoder.flush();
|
||||
},
|
||||
close,
|
||||
finish: async () => {
|
||||
logger.debug('Finishing encoding');
|
||||
await next();
|
||||
close();
|
||||
const buffer = muxToMp4(encodedChunks, {
|
||||
sampleRate: codecs.sampleRate,
|
||||
numberOfChannels: codecs.numberOfChannels,
|
||||
bitrate: codecs.targetBitrate,
|
||||
});
|
||||
return buffer;
|
||||
},
|
||||
[Symbol.dispose]: () => {
|
||||
close();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export type OpusStreamEncoder = ReturnType<typeof createStreamEncoder>;
|
||||
|
||||
Reference in New Issue
Block a user