mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-23 07:40:46 +08:00
fix: lint
This commit is contained in:
@@ -433,7 +433,9 @@ export const NbStoreNativeDBApis: NativeDBApis = {
|
||||
id: string,
|
||||
docId: string
|
||||
): Promise<DocIndexedClock | null> {
|
||||
return NbStore.getDocIndexedClock({ id, docId });
|
||||
return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
|
||||
clock ? { ...clock, timestamp: new Date(clock.timestamp) } : null
|
||||
);
|
||||
},
|
||||
setDocIndexedClock: function (
|
||||
id: string,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { appIconMap } from '@affine/core/utils';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -53,6 +53,7 @@ const appIcon = appIconMap[BUILD_CONFIG.appBuildType];
|
||||
|
||||
export function Recording() {
|
||||
const status = useRecordingStatus();
|
||||
const trackedNewRecordingIdsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
const t = useI18n();
|
||||
const textElement = useMemo(() => {
|
||||
@@ -101,42 +102,15 @@ export function Recording() {
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
let removed = false;
|
||||
if (!status || status.status !== 'new') return;
|
||||
if (trackedNewRecordingIdsRef.current.has(status.id)) return;
|
||||
|
||||
const handleRecordingStatusChanged = async (status: Status) => {
|
||||
if (removed) {
|
||||
return;
|
||||
}
|
||||
if (status?.status === 'new') {
|
||||
track.popup.$.recordingBar.toggleRecordingBar({
|
||||
type: 'Meeting record',
|
||||
appName: status.appName || 'System Audio',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
apis?.recording
|
||||
.getCurrentRecording()
|
||||
.then(status => {
|
||||
if (status) {
|
||||
return handleRecordingStatusChanged(status);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
// allow processing stopped event in tray menu as well:
|
||||
const unsubscribe = events?.recording.onRecordingStatusChanged(status => {
|
||||
if (status) {
|
||||
handleRecordingStatusChanged(status).catch(console.error);
|
||||
}
|
||||
trackedNewRecordingIdsRef.current.add(status.id);
|
||||
track.popup.$.recordingBar.toggleRecordingBar({
|
||||
type: 'Meeting record',
|
||||
appName: status.appName || 'System Audio',
|
||||
});
|
||||
|
||||
return () => {
|
||||
removed = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, []);
|
||||
}, [status]);
|
||||
|
||||
const handleStartRecording = useAsyncCallback(async () => {
|
||||
if (!status) {
|
||||
|
||||
@@ -476,15 +476,18 @@ export function newRecording(
|
||||
export async function startRecording(
|
||||
appGroup?: AppGroupInfo | number
|
||||
): Promise<RecordingStatus | null> {
|
||||
const previousState = recordingStateMachine.status;
|
||||
const state = recordingStateMachine.dispatch({
|
||||
type: 'START_RECORDING',
|
||||
appGroup: normalizeAppGroupInfo(appGroup),
|
||||
});
|
||||
|
||||
if (!state || state.status !== 'recording') {
|
||||
if (!state || state.status !== 'recording' || state === previousState) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let nativeId: string | undefined;
|
||||
|
||||
try {
|
||||
fs.ensureDirSync(SAVED_RECORDINGS_DIR);
|
||||
|
||||
@@ -494,6 +497,7 @@ export async function startRecording(
|
||||
format: 'opus',
|
||||
id: String(state.id),
|
||||
});
|
||||
nativeId = meta.id;
|
||||
|
||||
const filepath = assertRecordingFilepath(meta.filepath);
|
||||
const nextState = recordingStateMachine.dispatch({
|
||||
@@ -506,8 +510,15 @@ export async function startRecording(
|
||||
numberOfChannels: meta.channels,
|
||||
});
|
||||
|
||||
if (!nextState || nextState.nativeId !== meta.id) {
|
||||
throw new Error('Failed to attach native recording metadata');
|
||||
}
|
||||
|
||||
return nextState;
|
||||
} catch (error) {
|
||||
if (nativeId) {
|
||||
await cleanupAbandonedNativeRecording(nativeId);
|
||||
}
|
||||
logger.error('failed to start recording', error);
|
||||
return recordingStateMachine.dispatch({
|
||||
type: 'CREATE_BLOCK_FAILED',
|
||||
@@ -600,6 +611,16 @@ export async function readRecordingFile(filepath: string) {
|
||||
return fsp.readFile(normalizedPath);
|
||||
}
|
||||
|
||||
async function cleanupAbandonedNativeRecording(nativeId: string) {
|
||||
try {
|
||||
const artifact = getNativeModule().stopRecording(nativeId);
|
||||
const filepath = assertRecordingFilepath(artifact.filepath);
|
||||
await fsp.rm(filepath, { force: true });
|
||||
} catch (error) {
|
||||
logger.error('failed to cleanup abandoned native recording', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleBlockCreationSuccess(id: number) {
|
||||
recordingStateMachine.dispatch({
|
||||
type: 'CREATE_BLOCK_SUCCESS',
|
||||
|
||||
@@ -172,7 +172,9 @@ class TrayState implements Disposable {
|
||||
logger.info(
|
||||
`User action: Start Recording Meeting (${appGroup.name})`
|
||||
);
|
||||
startRecording(appGroup);
|
||||
startRecording(appGroup).catch(err => {
|
||||
logger.error('Failed to start recording:', err);
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -188,7 +190,9 @@ class TrayState implements Disposable {
|
||||
logger.info(
|
||||
'User action: Start Recording Meeting (System audio)'
|
||||
);
|
||||
startRecording();
|
||||
startRecording().catch(err => {
|
||||
logger.error('Failed to start recording:', err);
|
||||
});
|
||||
},
|
||||
},
|
||||
...appMenuItems,
|
||||
|
||||
@@ -433,7 +433,9 @@ export const NbStoreNativeDBApis: NativeDBApis = {
|
||||
id: string,
|
||||
docId: string
|
||||
): Promise<DocIndexedClock | null> {
|
||||
return NbStore.getDocIndexedClock({ id, docId });
|
||||
return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
|
||||
clock ? { ...clock, timestamp: new Date(clock.timestamp) } : null
|
||||
);
|
||||
},
|
||||
setDocIndexedClock: function (
|
||||
id: string,
|
||||
|
||||
@@ -156,8 +156,8 @@ impl InterleavedResampler {
|
||||
}
|
||||
let out_len = blocks[0].len();
|
||||
for i in 0..out_len {
|
||||
for ch in 0..self.channels {
|
||||
out.push(blocks[ch][i]);
|
||||
for channel in blocks.iter().take(self.channels) {
|
||||
out.push(channel[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,14 @@ impl OggOpusWriter {
|
||||
let channels = if channels > 1 { Channels::Stereo } else { Channels::Mono };
|
||||
|
||||
let sample_rate = ENCODE_SAMPLE_RATE;
|
||||
let mut encoder =
|
||||
Encoder::new(sample_rate, channels, Application::Audio).map_err(|e| RecordingError::Encoding(e.to_string()))?;
|
||||
let pre_skip = u16::try_from(
|
||||
encoder
|
||||
.lookahead()
|
||||
.map_err(|e| RecordingError::Encoding(e.to_string()))?,
|
||||
)
|
||||
.map_err(|_| RecordingError::Encoding("invalid encoder lookahead".into()))?;
|
||||
let resampler = if source_sample_rate != sample_rate.as_i32() as u32 {
|
||||
Some(InterleavedResampler::new(
|
||||
source_sample_rate,
|
||||
@@ -204,18 +212,16 @@ impl OggOpusWriter {
|
||||
let mut writer = PacketWriter::new(BufWriter::new(file));
|
||||
|
||||
let stream_serial: u32 = rand::random();
|
||||
write_opus_headers(&mut writer, stream_serial, channels, sample_rate)?;
|
||||
write_opus_headers(&mut writer, stream_serial, channels, sample_rate, pre_skip)?;
|
||||
|
||||
let frame_samples = FrameSize::Ms20.samples(sample_rate);
|
||||
let encoder =
|
||||
Encoder::new(sample_rate, channels, Application::Audio).map_err(|e| RecordingError::Encoding(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
writer,
|
||||
encoder,
|
||||
frame_samples,
|
||||
pending: Vec::new(),
|
||||
granule_position: 0,
|
||||
granule_position: u64::from(pre_skip),
|
||||
samples_written: 0,
|
||||
channels,
|
||||
sample_rate,
|
||||
@@ -316,12 +322,13 @@ fn write_opus_headers(
|
||||
stream_serial: u32,
|
||||
channels: Channels,
|
||||
sample_rate: OpusSampleRate,
|
||||
pre_skip: u16,
|
||||
) -> RecordingResult<()> {
|
||||
let mut opus_head = Vec::with_capacity(19);
|
||||
opus_head.extend_from_slice(b"OpusHead");
|
||||
opus_head.push(1); // version
|
||||
opus_head.push(channels.as_usize() as u8);
|
||||
opus_head.extend_from_slice(&0u16.to_le_bytes()); // pre-skip
|
||||
opus_head.extend_from_slice(&pre_skip.to_le_bytes());
|
||||
opus_head.extend_from_slice(&(sample_rate.as_i32() as u32).to_le_bytes());
|
||||
opus_head.extend_from_slice(&0i16.to_le_bytes()); // output gain
|
||||
opus_head.push(0); // channel mapping
|
||||
@@ -374,6 +381,7 @@ struct ActiveRecording {
|
||||
|
||||
static ACTIVE_RECORDINGS: LazyLock<Mutex<HashMap<String, ActiveRecording>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
static START_RECORDING_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
fn now_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
@@ -382,14 +390,18 @@ fn now_millis() -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn new_recording_id() -> String {
|
||||
format!("{}-{:08x}", now_millis(), rand::random::<u32>())
|
||||
}
|
||||
|
||||
fn sanitize_id(id: Option<String>) -> String {
|
||||
let raw = id.unwrap_or_else(|| format!("{}", now_millis()));
|
||||
let raw = id.unwrap_or_else(new_recording_id);
|
||||
let filtered: String = raw
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
|
||||
.collect();
|
||||
if filtered.is_empty() {
|
||||
format!("{}", now_millis())
|
||||
new_recording_id()
|
||||
} else {
|
||||
filtered
|
||||
}
|
||||
@@ -427,13 +439,13 @@ fn start_capture(opts: &RecordingStartOptions, tx: Sender<Vec<f32>>) -> Result<(
|
||||
let session = if let Some(app_id) = opts.app_process_id {
|
||||
ShareableContent::tap_audio_with_callback(app_id, callback)?
|
||||
} else {
|
||||
let excluded_apps = build_excluded_refs(opts.exclude_process_ids.as_ref().map(|v| v.as_slice()).unwrap_or(&[]))?;
|
||||
let excluded_apps = build_excluded_refs(opts.exclude_process_ids.as_deref().unwrap_or(&[]))?;
|
||||
let excluded_refs: Vec<&ApplicationInfo> = excluded_apps.iter().collect();
|
||||
ShareableContent::tap_global_audio_with_callback(Some(excluded_refs), callback)?
|
||||
};
|
||||
let sample_rate = session.get_sample_rate()?.round().clamp(1.0, f64::MAX) as u32;
|
||||
let channels = session.get_channels()?;
|
||||
return Ok((PlatformCapture::Mac(session), sample_rate, channels));
|
||||
Ok((PlatformCapture::Mac(session), sample_rate, channels))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -474,14 +486,28 @@ fn spawn_worker(
|
||||
|
||||
#[napi]
|
||||
pub fn start_recording(opts: RecordingStartOptions) -> Result<RecordingSessionMeta> {
|
||||
if let Some(fmt) = opts.format.as_deref() {
|
||||
if fmt.to_ascii_lowercase() != "opus" {
|
||||
return Err(RecordingError::InvalidFormat(fmt.to_string()).into());
|
||||
if let Some(fmt) = opts.format.as_deref()
|
||||
&& !fmt.eq_ignore_ascii_case("opus")
|
||||
{
|
||||
return Err(RecordingError::InvalidFormat(fmt.to_string()).into());
|
||||
}
|
||||
|
||||
let _start_lock = START_RECORDING_LOCK
|
||||
.lock()
|
||||
.map_err(|_| RecordingError::Start("lock poisoned".into()))?;
|
||||
let output_dir = validate_output_dir(&opts.output_dir)?;
|
||||
let id = sanitize_id(opts.id.clone());
|
||||
|
||||
{
|
||||
let recordings = ACTIVE_RECORDINGS
|
||||
.lock()
|
||||
.map_err(|_| RecordingError::Start("lock poisoned".into()))?;
|
||||
|
||||
if recordings.contains_key(&id) {
|
||||
return Err(RecordingError::Start("duplicate recording id".into()).into());
|
||||
}
|
||||
}
|
||||
|
||||
let output_dir = validate_output_dir(&opts.output_dir)?;
|
||||
let id = sanitize_id(opts.id.clone());
|
||||
let filepath = output_dir.join(format!("{id}.opus"));
|
||||
if filepath.exists() {
|
||||
fs::remove_file(&filepath)?;
|
||||
@@ -511,10 +537,6 @@ pub fn start_recording(opts: RecordingStartOptions) -> Result<RecordingSessionMe
|
||||
.lock()
|
||||
.map_err(|_| RecordingError::Start("lock poisoned".into()))?;
|
||||
|
||||
if recordings.contains_key(&id) {
|
||||
return Err(RecordingError::Start("duplicate recording id".into()).into());
|
||||
}
|
||||
|
||||
recordings.insert(
|
||||
id,
|
||||
ActiveRecording {
|
||||
@@ -540,7 +562,7 @@ pub fn stop_recording(id: String) -> Result<RecordingArtifact> {
|
||||
drop(entry.sender.take());
|
||||
|
||||
let handle = entry.worker.take().ok_or(RecordingError::Join)?;
|
||||
let artifact = handle.join().map_err(|_| RecordingError::Join)?.map_err(|e| e)?;
|
||||
let artifact = handle.join().map_err(|_| RecordingError::Join)??;
|
||||
|
||||
Ok(artifact)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user