Skip to content

Conversation

@darkskygit
Copy link
Member

@darkskygit darkskygit commented Dec 30, 2025

Summary by CodeRabbit

Release Notes

  • Refactor

    • Restructured audio recording system to use centralized state management with enhanced native integration for improved stability and lifecycle management.
    • Simplified internal recording encoder logic while maintaining consistent user-facing functionality.
  • Chores

    • Added dependencies to support native audio encoding and processing improvements.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

The changes introduce native-module-driven audio recording via Opus encoding in Ogg containers, replacing in-process Opus handling. A new recording state machine orchestrates lifecycle management, native audio capture integrates through an AudioCallback abstraction, and public APIs expose startRecording/stopRecording functions. Opus streaming encoder is removed from the frontend layer.

Changes

Cohort / File(s) Summary
Dependency Updates
Cargo.toml, packages/frontend/native/media_capture/Cargo.toml
Added ogg, opus-codec, rand, thiserror, crossbeam-channel dependencies to support native Opus encoding and audio channel management.
Recording Feature Refactoring
packages/frontend/apps/electron/src/main/recording/feature.ts, packages/frontend/apps/electron/src/main/recording/index.ts
Replaced in-file Map-based recording storage and direct Opus encoding with state-machine-driven lifecycle. Made startRecording async returning Promise<RecordingStatus | null>; updated stopRecording to async returning Promise<SerializedRecordingStatus | void>. Removed getRawAudioBuffers and readyRecording handlers; now orchestrates native module calls and state transitions.
Recording State Machine
packages/frontend/apps/electron/src/main/recording/state-machine.ts
Added ATTACH_NATIVE_RECORDING event variant with metadata fields (id, nativeId, startTime, filepath, sampleRate, numberOfChannels). Extended SAVE_RECORDING to include optional sampleRate and numberOfChannels propagation. Introduced handleAttachNativeRecording handler.
Recording Types & API
packages/frontend/apps/electron/src/main/recording/types.ts
Expanded TappableAppInfo and AppGroupInfo with process/bundle identifiers; removed Recording interface; extended RecordingStatus with nativeId, sampleRate, numberOfChannels fields.
Core Opus Encoding
packages/frontend/core/src/utils/opus-encoding.ts
Removed createStreamEncoder and OpusStreamEncoder type, eliminating streaming-based Opus encoding API.
Native Module API
packages/frontend/native/index.d.ts, packages/frontend/native/index.js
Added startRecording(opts: RecordingStartOptions) and stopRecording(id: string) public exports, exposing new native recording interfaces (RecordingArtifact, RecordingSessionMeta, RecordingStartOptions).
Audio Callback Abstraction
packages/frontend/native/media_capture/src/audio_callback.rs, packages/frontend/native/media_capture/src/lib.rs
New public AudioCallback enum abstracting callbacks via ThreadsafeFunction (Js) or crossbeam channel (Channel) with unified call() dispatch method.
macOS Audio Capture
packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs, packages/frontend/native/media_capture/src/macos/tap_audio.rs
Refactored tap_audio/tap_global_audio to accept AudioCallback instead of Arc; introduced private \_with_callback variants; updated AggregateDevice and AggregateDeviceManager to use AudioCallback abstraction internally.
Windows Audio Capture
packages/frontend/native/media_capture/src/windows/audio_capture.rs
Updated start_recording signature to accept AudioCallback and optional target_sample_rate parameter; replaced ThreadsafeFunction.call with direct AudioCallback.call(mixed).
Windows Screen Capture
packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs
Added pub(crate) tap_audio_with_callback and tap_global_audio_with_callback variants; existing public methods now wrap ThreadsafeFunction into AudioCallback::Js and delegate.
Native Recording Module
packages/frontend/native/media_capture/src/recording.rs
New comprehensive module implementing start_recording and stop_recording N-API functions. Manages active recordings via global ACTIVE_RECORDINGS map, spawns worker threads for Opus/Ogg encoding with optional resampling, handles audio chunking via crossbeam channels, tracks granule positions, and generates RecordingArtifact with duration/size metadata.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer as Electron Renderer
    participant Feature as Recording Feature
    participant StateMachine as State Machine
    participant Native as Native Module
    participant Capture as Audio Capture (macOS/Windows)
    participant Encoder as Opus Encoder (Worker)
    participant FileSystem as File System

    rect rgb(100, 150, 200)
    note over Renderer,Encoder: Start Recording Flow
    
    Renderer->>Feature: startRecording(appGroup)
    Feature->>StateMachine: dispatch CREATE_BLOCK
    StateMachine-->>Feature: RecordingStatus (id, pending)
    Feature->>Native: startRecording(opts)
    Native->>Capture: create capture session with AudioCallback
    Capture-->>Native: RecordingSessionMeta (id, filepath, startedAt)
    Native-->>Feature: RecordingSessionMeta
    Feature->>StateMachine: dispatch ATTACH_NATIVE_RECORDING
    StateMachine-->>Feature: RecordingStatus (with nativeId, filepath, sampleRate)
    Feature-->>Renderer: Promise<RecordingStatus>
    end

    rect rgb(150, 180, 220)
    note over Capture,FileSystem: Recording & Encoding Flow
    
    Capture->>Capture: capture audio frames
    Capture->>Encoder: AudioCallback.call(samples) via channel
    Encoder->>Encoder: encode frame (20ms Opus)
    Encoder->>Encoder: track granule position
    Encoder->>FileSystem: write Opus frames to Ogg file
    end

    rect rgb(100, 150, 200)
    note over Renderer,FileSystem: Stop Recording Flow
    
    Renderer->>Feature: stopRecording(id)
    Feature->>Native: stopRecording(id)
    Native->>Capture: stop capture session
    Capture-->>Native: (capture halted)
    Native->>Encoder: signal end-of-stream
    Encoder->>Encoder: finalize Ogg (duration, size)
    Encoder->>FileSystem: close file
    FileSystem-->>Native: RecordingArtifact (filepath, size, durationMs)
    Native-->>Feature: RecordingArtifact
    Feature->>StateMachine: dispatch SAVE_RECORDING
    StateMachine-->>Feature: RecordingStatus (saved, with metadata)
    Feature-->>Renderer: Promise<SerializedRecordingStatus>
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 Opus flows through native streams,
AudioCallback threads and dreams,
State machines dance with Ogg's sweet song,
Recording captured, whole and strong!
From electron down to Rust we go—
A complete refactoring show. 🎙️✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: record encoding' accurately describes the main change—adding native recording with Opus encoding functionality across multiple modules.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (5)
packages/frontend/native/media_capture/src/audio_callback.rs (1)

15-29: Consider implications of silently dropping audio samples.

The Channel variant uses try_send, which drops samples when the channel is full. While this prevents blocking the audio thread, it could lead to gaps in recordings without any indication to the caller.

Consider:

  • Adding a counter or metric to track dropped samples for observability
  • Using a bounded channel with appropriate capacity based on expected latency
  • Documenting the expected consumer behavior to prevent buffer overruns
packages/frontend/native/nbstore/src/indexer_sync.rs (1)

55-111: Tests cover basic functionality.

The unit tests validate the core CRUD operations for indexed clocks. Consider adding tests for:

  • Updating an existing indexed clock (upsert behavior)
  • Handling non-existent doc_ids in get/clear operations
  • Concurrent updates if that's a concern
packages/frontend/native/media_capture/src/recording.rs (3)

104-168: Potential data loss in resampler warm-up phase.

The resampler discards the first output block (lines 153-156) during the "warm-up" phase. While this is common for some resampler algorithms to avoid initial transients, it may cause a small amount of audio data loss at the start of recordings. Consider whether this is acceptable for your use case or if the warm-up samples should be handled differently.


384-385: Global state mutex could cause deadlock if held across async boundaries.

The ACTIVE_RECORDINGS mutex is locked during start_recording and stop_recording. If the lock is held while calling blocking operations, it could cause contention. The current implementation appears safe since the lock is released before spawning the worker thread, but be cautious about future modifications.


434-478: Unreachable code warning on unsupported platforms.

The #[cfg(not(any(target_os = "macos", target_os = "windows")))] block (lines 472-477) returns an error but the let _ = opts; let _ = tx; statements are needed to suppress unused variable warnings. This is fine but consider using #[allow(unused_variables)] for clarity.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 95ef04f and 0d3ac5d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • Cargo.toml
  • packages/common/nbstore/src/impls/sqlite/db.ts
  • packages/common/nbstore/src/impls/sqlite/index.ts
  • packages/common/nbstore/src/impls/sqlite/indexer-sync.ts
  • packages/common/nbstore/src/storage/index.ts
  • packages/frontend/apps/android/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/android/src/plugins/nbstore/index.ts
  • packages/frontend/apps/electron-renderer/src/background-worker/index.ts
  • packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx
  • packages/frontend/apps/electron/src/helper/nbstore/handlers.ts
  • packages/frontend/apps/electron/src/main/protocol.ts
  • packages/frontend/apps/electron/src/main/recording/feature.ts
  • packages/frontend/apps/electron/src/main/recording/index.ts
  • packages/frontend/apps/electron/src/main/recording/state-machine.ts
  • packages/frontend/apps/electron/src/main/recording/types.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
  • packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts
  • packages/frontend/core/src/modules/workspace-engine/impls/local.ts
  • packages/frontend/core/src/utils/opus-encoding.ts
  • packages/frontend/native/index.d.ts
  • packages/frontend/native/index.js
  • packages/frontend/native/media_capture/Cargo.toml
  • packages/frontend/native/media_capture/src/audio_callback.rs
  • packages/frontend/native/media_capture/src/lib.rs
  • packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs
  • packages/frontend/native/media_capture/src/macos/tap_audio.rs
  • packages/frontend/native/media_capture/src/recording.rs
  • packages/frontend/native/media_capture/src/windows/audio_capture.rs
  • packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs
  • packages/frontend/native/nbstore/src/doc.rs
  • packages/frontend/native/nbstore/src/indexer_sync.rs
  • packages/frontend/native/nbstore/src/lib.rs
  • packages/frontend/native/schema/src/lib.rs
💤 Files with no reviewable changes (3)
  • packages/frontend/core/src/utils/opus-encoding.ts
  • packages/frontend/apps/electron/src/main/recording/index.ts
  • packages/frontend/apps/electron-renderer/src/background-worker/index.ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/frontend/apps/ios/**/*.{ts,tsx}

📄 CodeRabbit inference engine (packages/frontend/apps/ios/AGENTS.md)

packages/frontend/apps/ios/**/*.{ts,tsx}: Use TypeScript as the programming language
Expose JavaScript APIs to native iOS code through window object for Capacitor integration
Implement getCurrentServerBaseUrl() API for native iOS bridge
Implement getCurrentI18nLocale() API for native iOS bridge
Implement getAiButtonFeatureFlag() API for native iOS bridge
Implement getCurrentWorkspaceId() API for native iOS bridge
Implement getCurrentDocId() API for native iOS bridge
Implement getCurrentDocContentInMarkdown() API for native iOS bridge
Implement createNewDocByMarkdownInCurrentWorkspace() API for native iOS bridge
Enable TypeScript strict mode

Files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
packages/frontend/apps/ios/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/frontend/apps/ios/AGENTS.md)

Follow ESLint/Prettier configuration from workspace root

Files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
🧠 Learnings (9)
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentWorkspaceId() API for native iOS bridge

Applied to files:

  • packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts
  • packages/frontend/core/src/modules/workspace-engine/impls/local.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentDocId() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/android/src/plugins/nbstore/index.ts
  • packages/common/nbstore/src/impls/sqlite/db.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement createNewDocByMarkdownInCurrentWorkspace() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentDocContentInMarkdown() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/electron/src/main/recording/feature.ts
  • packages/frontend/apps/ios/src/plugins/nbstore/index.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Expose JavaScript APIs to native iOS code through window object for Capacitor integration

Applied to files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentI18nLocale() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts
  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getAiButtonFeatureFlag() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Use TypeScript as the programming language

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Enable TypeScript strict mode

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
🧬 Code graph analysis (17)
packages/frontend/native/index.js (1)
packages/common/y-octo/node/index.js (1)
  • nativeBinding (7-7)
packages/frontend/core/src/modules/workspace-engine/impls/cloud.ts (1)
packages/common/nbstore/src/impls/sqlite/indexer-sync.ts (1)
  • SqliteIndexerSyncStorage (8-38)
packages/common/nbstore/src/impls/sqlite/indexer-sync.ts (2)
packages/common/nbstore/src/impls/sqlite/db.ts (2)
  • NativeDBConnection (146-201)
  • SqliteNativeDBOptions (12-16)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts (1)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/common/nbstore/src/impls/sqlite/index.ts (1)
packages/common/nbstore/src/impls/sqlite/indexer-sync.ts (1)
  • SqliteIndexerSyncStorage (8-38)
packages/frontend/core/src/modules/workspace-engine/impls/local.ts (1)
packages/common/nbstore/src/impls/sqlite/indexer-sync.ts (1)
  • SqliteIndexerSyncStorage (8-38)
packages/frontend/native/index.d.ts (1)
packages/frontend/apps/electron/src/main/recording/feature.ts (2)
  • startRecording (474-516)
  • stopRecording (526-583)
packages/frontend/native/nbstore/src/indexer_sync.rs (2)
packages/frontend/native/nbstore/src/lib.rs (4)
  • get_doc_indexed_clock (248-260)
  • clear_doc_indexed_clock (279-286)
  • new (101-105)
  • new (589-593)
packages/frontend/native/nbstore/src/doc.rs (1)
  • get_storage (265-270)
packages/frontend/native/nbstore/src/lib.rs (2)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/native/nbstore/src/indexer_sync.rs (3)
  • get_doc_indexed_clock (6-20)
  • set_doc_indexed_clock (22-43)
  • clear_doc_indexed_clock (45-52)
packages/frontend/apps/android/src/plugins/nbstore/index.ts (2)
packages/common/nbstore/src/storage/doc.ts (1)
  • CrawlResult (22-26)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs (4)
packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs (5)
  • tap_audio_with_callback (681-709)
  • tap_audio (712-720)
  • new (104-110)
  • new (529-533)
  • tap_global_audio (740-748)
packages/frontend/native/media_capture/src/windows/audio_capture.rs (2)
  • start_recording (220-395)
  • new (32-49)
packages/frontend/native/media_capture/src/macos/tap_audio.rs (3)
  • new (69-116)
  • new (580-593)
  • new (955-961)
packages/frontend/native/media_capture/src/windows/microphone_listener.rs (2)
  • new (202-219)
  • new (363-393)
packages/common/nbstore/src/impls/sqlite/db.ts (1)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/apps/android/src/plugins/nbstore/definitions.ts (1)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/native/media_capture/src/macos/tap_audio.rs (1)
packages/frontend/native/media_capture/src/recording.rs (1)
  • start_capture (434-478)
packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx (2)
packages/frontend/apps/electron/src/preload/electron-api.ts (1)
  • apis (278-281)
packages/frontend/apps/electron/src/main/recording/state-machine.ts (1)
  • status (61-63)
packages/frontend/apps/ios/src/plugins/nbstore/index.ts (1)
packages/frontend/native/index.d.ts (1)
  • DocIndexedClock (141-145)
packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs (2)
packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs (5)
  • tap_audio_with_callback (235-242)
  • process_id (130-132)
  • tap_audio (245-256)
  • new (66-72)
  • new (192-203)
packages/frontend/native/media_capture/src/macos/tap_audio.rs (3)
  • new (69-116)
  • new (580-593)
  • new (955-961)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: y-octo binding test on x86_64-apple-darwin
  • GitHub Check: E2E Test (8)
  • GitHub Check: E2E Test (10)
  • GitHub Check: Build AFFiNE native (aarch64-apple-darwin)
  • GitHub Check: E2E Test (2)
  • GitHub Check: Build AFFiNE native (x86_64-apple-darwin)
  • GitHub Check: E2E Test (1)
  • GitHub Check: E2E Test (5)
  • GitHub Check: E2E Test (6)
  • GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
  • GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
  • GitHub Check: E2E Mobile Test (4)
  • GitHub Check: E2E Mobile Test (5)
  • GitHub Check: miri code check
  • GitHub Check: E2E BlockSuite Cross Browser Test (1, chromium)
  • GitHub Check: E2E BlockSuite Cross Browser Test (1, firefox)
  • GitHub Check: Lint
  • GitHub Check: Analyze (typescript, affine)
  • GitHub Check: Analyze (javascript, affine)
  • GitHub Check: Typecheck
🔇 Additional comments (55)
packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx (1)

139-139: LGTM!

The empty dependency array is correct here since the effect only needs to run once on mount to set up the recording status subscription. The imported apis and events are stable module-level imports, and all other variables are defined within the effect's scope.

packages/frontend/native/media_capture/Cargo.toml (3)

14-14: Good refactoring: crossbeam-channel is now cross-platform.

Moving crossbeam-channel from Windows-specific dependencies to global dependencies indicates it's now used across platforms, which aligns with the broader audio recording capabilities introduced in this PR.

Also applies to: 19-19


17-17: LGTM: ogg dependency addition.

The ogg workspace dependency correctly supports the Ogg container format for the new encoding functionality.


18-18: No action needed—opus-codec 0.1.2 is the latest stable version with no known security vulnerabilities.

The dependency is already pinned to the latest release on crates.io and has no published RustSec/GHSA advisories. The version is secure and current.

Cargo.toml (1)

67-67: Version 0.9 is current with no known security vulnerabilities.

The ogg crate latest stable version is 0.9.2, so the 0.9 specification will resolve correctly. No security advisories are present.

packages/frontend/native/nbstore/src/doc.rs (2)

44-48: LGTM! Consistent with existing update pattern.

The indexer_sync table update correctly follows the established pattern for maintaining referential integrity when the space_id changes. The placement between clocks and peer_clocks updates is appropriate.


215-218: LGTM! Proper cleanup within transaction.

The indexer_sync deletion is correctly included in the delete_doc transaction, ensuring atomic cleanup of all related records.

packages/frontend/native/nbstore/src/lib.rs (3)

7-7: LGTM! Module and struct follow existing patterns.

The new indexer_sync module and DocIndexedClock struct are well-defined and consistent with the codebase conventions. The #[napi(object)] attribute correctly exposes the struct to JavaScript.

Also applies to: 59-65


247-276: LGTM! Methods follow established patterns.

The get_doc_indexed_clock and set_doc_indexed_clock methods correctly delegate to the storage layer and maintain consistency with the existing API surface.


278-286: LGTM! Clear method completes the API surface.

The clear_doc_indexed_clock method provides the necessary deletion functionality and maintains consistency with the rest of the codebase.

packages/frontend/native/nbstore/src/indexer_sync.rs (3)

6-20: LGTM! Safe and type-safe query implementation.

The get_doc_indexed_clock method correctly uses sqlx::query! with proper type annotations and safely handles optional results.


22-43: LGTM! Correct upsert implementation.

The set_doc_indexed_clock method properly implements an upsert pattern using ON CONFLICT, ensuring idempotent updates to the indexed clock state.


45-52: LGTM! Clean delete implementation.

The clear_doc_indexed_clock method correctly deletes the indexed clock record for the specified document.

packages/common/nbstore/src/storage/index.ts (1)

95-95: LGTM! Consistent with existing exports.

The indexer-sync export follows the established pattern and correctly expands the public API surface.

packages/frontend/native/media_capture/src/lib.rs (1)

11-13: LGTM! Module declarations follow Rust conventions.

The new audio_callback and recording modules are correctly declared as public, expanding the library's API surface appropriately.

packages/frontend/native/schema/src/lib.rs (1)

89-100: LGTM! Migration adds indexer_sync table.

The new migration correctly creates the indexer_sync table with appropriate schema. Note that this migration is irreversible (no down migration), which is acceptable for a new table that doesn't affect existing data.

packages/frontend/apps/electron/src/main/protocol.ts (3)

130-135: LGTM! Domain patterns correctly match affine domains.

The affineDomains regex patterns properly match all affine-related domains including subdomains, using case-insensitive matching.


231-246: LGTM! Logic correctly applies CORS for affine domains.

The updated onHeadersReceived handler correctly identifies requests to affine domains and applies CORS headers. The logic properly maintains separation between assets protocol handling and HTTP/HTTPS affine domain handling.


175-184: The Access-Control-Allow-Origin: * is scoped to affine-controlled domains only (lines 241-245 restrict allowCors calls to responses matching affineDomains regex), so it doesn't expose resources to arbitrary origins. This wildcard policy is intentional for supporting signed blob redirects to *.usercontent.affine.pro within the Electron app's protocol handler. The backend confirms blob URLs are cryptographically signed and time-limited (1-hour expiration), mitigating exposure risk. No action needed.

packages/frontend/native/index.js (1)

582-583: LGTM!

The new startRecording and stopRecording exports follow the established pattern for exposing native bindings. This is auto-generated code by NAPI-RS, and the additions are consistent with the existing export structure.

packages/frontend/apps/electron/src/helper/nbstore/handlers.ts (1)

33-35: LGTM!

The new getDocIndexedClock, setDocIndexedClock, and clearDocIndexedClock handlers follow the established binding pattern and are correctly bound to the POOL instance.

packages/common/nbstore/src/impls/sqlite/index.ts (1)

7-7: LGTM!

The SqliteIndexerSyncStorage is properly imported, exported, and registered in the sqliteStorages array, following the established pattern for other storage types in this module.

Also applies to: 15-15, 23-23

packages/frontend/apps/electron/src/main/recording/types.ts (1)

43-45: LGTM!

The new optional fields (nativeId, sampleRate, numberOfChannels) on RecordingStatus appropriately support the refactored state-machine-driven recording architecture. These fields are correctly marked optional since they're populated during different recording lifecycle stages (e.g., after ATTACH_NATIVE_RECORDING and SAVE_RECORDING actions).

packages/common/nbstore/src/impls/sqlite/db.ts (1)

33-43: LGTM!

The new getDocIndexedClock, setDocIndexedClock, and clearDocIndexedClock methods extend the NativeDBApis interface consistently with the existing clock-related methods. The signatures properly use Date for timestamps and align with the DocIndexedClock type structure.

packages/frontend/native/index.d.ts (2)

44-73: LGTM!

The new recording interfaces (RecordingArtifact, RecordingSessionMeta, RecordingStartOptions) and function declarations (startRecording, stopRecording) are well-structured. The synchronous signatures are appropriate for native bindings that return immediately with session metadata or artifacts.


99-101: LGTM!

The DocIndexedClock interface and corresponding DocStoragePool methods are properly declared. The async Promise-based signatures for pool operations are consistent with the existing clock-related methods in the class.

Also applies to: 141-145

packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts (2)

1-1: LGTM!

The import of DocIndexedClock from @affine/nbstore aligns the iOS plugin with the shared type definitions.


190-203: LGTM!

The new getDocIndexedClock, setDocIndexedClock, and clearDocIndexedClock methods extend the NbStorePlugin interface consistently with the existing API patterns. The use of indexedClock: number (instead of Date) for the timestamp parameter is consistent with how other methods in this file handle timestamps for the Capacitor bridge serialization.

packages/frontend/native/media_capture/src/windows/audio_capture.rs (2)

220-223: LGTM!

The refactored start_recording function correctly accepts the new AudioCallback abstraction and optional SampleRate. The target rate selection logic properly defaults to the minimum of microphone and loopback sample rates when not specified.


346-349: LGTM!

The callback invocation is correctly updated to use the unified AudioCallback::call method, aligning with the cross-platform callback abstraction.

packages/frontend/apps/electron/src/main/recording/state-machine.ts (3)

16-24: LGTM!

The new ATTACH_NATIVE_RECORDING event is well-structured with all necessary fields for native recording metadata attachment.


181-208: LGTM!

The handleAttachNativeRecording method correctly validates both the recording ID and status before attaching native metadata. The error logging for invalid states is appropriate for debugging.


282-301: LGTM!

The handleSaveRecording method is correctly updated to accept and propagate optional sampleRate and numberOfChannels parameters.

packages/frontend/apps/ios/src/plugins/nbstore/index.ts (1)

425-443: LGTM!

The setDocIndexedClock correctly converts Date to timestamp via getTime(), and clearDocIndexedClock properly delegates to the native store.

packages/frontend/apps/android/src/plugins/nbstore/index.ts (1)

341-346: LGTM!

The crawlDocData explicit typing, setDocIndexedClock Date-to-timestamp conversion, and clearDocIndexedClock delegation are correctly implemented.

Also applies to: 425-443

packages/common/nbstore/src/impls/sqlite/indexer-sync.ts (1)

1-38: LGTM!

The SqliteIndexerSyncStorage implementation correctly extends IndexerSyncStorageBase and follows the established patterns of other SQLite storage implementations:

  • Shared connection via share() utility
  • Static identifier for storage registration
  • Methods properly delegate to the wrapped native DB APIs
packages/frontend/apps/android/src/plugins/nbstore/definitions.ts (1)

1-1: LGTM!

The NbStorePlugin interface extensions correctly define the native bridge contract:

  • DocIndexedClock type import for return type specification
  • The ftsGetMatches return type syntax change is a minor cleanup (...[] vs Array<...>)
  • New methods correctly use number for indexedClock parameter to match native bridge serialization

Note: The DocIndexedClock return type in getDocIndexedClock expects a Date for timestamp, but the native bridge will return a number. The transformation should occur in index.ts (see related comment on that file).

Also applies to: 187-187, 190-203

packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs (2)

235-256: LGTM!

The tap_audio_with_callback variant correctly:

  • Converts optional u32 to Option<SampleRate>
  • Delegates to the unified start_recording function

The existing tap_audio is cleanly refactored to wrap the callback in AudioCallback::Js and delegate to the new method.


258-279: LGTM!

The tap_global_audio_with_callback follows the same pattern and correctly delegates to start_recording. The refactor aligns Windows behavior with the macOS callback-based architecture.

packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs (3)

681-709: LGTM - Clean internal API for callback abstraction.

The internal tap_audio_with_callback method properly accepts the AudioCallback abstraction, allowing both JavaScript and channel-based callbacks. This aligns well with the Windows implementation pattern shown in the relevant snippets.


711-720: LGTM - Public API preserved with clean delegation.

The public N-API methods (tap_audio and tap_global_audio) correctly wrap the ThreadsafeFunction in AudioCallback::Js before delegating to the internal callback methods. This maintains backward compatibility while enabling the new native recording flow.

Also applies to: 739-748


722-737: LGTM - Global audio tap refactored consistently.

The tap_global_audio_with_callback method follows the same pattern as tap_audio_with_callback, providing a unified internal API for both single-app and global audio capture scenarios.

packages/frontend/native/media_capture/src/recording.rs (4)

559-581: LGTM - Stop recording handles cleanup correctly.

The stop_recording function properly:

  1. Acquires the lock and removes the entry atomically
  2. Stops the capture
  3. Drops the sender to signal EOF to the worker
  4. Joins the worker thread to get the artifact

170-230: LGTM - OggOpusWriter implementation is solid.

The encoder setup correctly:

  • Handles mono/stereo channels
  • Creates resampler when source rate differs from 48kHz encode rate
  • Writes proper Opus headers with vendor string
  • Uses 20ms frame size (standard for Opus)

279-320: The end-of-stream implementation is spec-compliant with RFC 7845.

The empty packet with EndStream flag properly marks the end of the logical Ogg/Opus stream per RFC 7845, which states the stream SHOULD end with a page marked with the "end of stream" flag. The granule position is correctly passed through, enabling proper end-trimming signaling. This approach is standard and will be correctly handled by spec-compliant Ogg/Opus players.


356-376: Verify unsafe impl Send is sound for PlatformCapture.

The unsafe impl Send for PlatformCapture (line 363) asserts thread-safety. Ensure that both AudioCaptureSession types (macOS and Windows) are actually safe to send between threads.

#!/bin/bash
# Check if AudioCaptureSession types implement Send or contain non-Send types
rg -n "pub struct AudioCaptureSession" packages/frontend/native/media_capture/src/
rg -n "Arc<.*Mutex" packages/frontend/native/media_capture/src/macos/tap_audio.rs packages/frontend/native/media_capture/src/windows/audio_capture.rs
packages/frontend/native/media_capture/src/macos/tap_audio.rs (3)

240-306: LGTM - AudioCallback integration in audio processing loop.

The start method correctly uses the new AudioCallback::call(mixed_samples) API. The callback is cloned into the RcBlock closure, which is appropriate for the callback's use in the audio I/O block.


611-652: LGTM - start_capture properly stores and uses AudioCallback.

The method correctly:

  1. Stores the callback for potential device switch handling (line 614)
  2. Clones the callback when starting the initial stream (line 633)
  3. Uses the stored callback reference in device change listeners

573-576: LGTM - Field type update is consistent.

The audio_callback field type change from Option<Arc<ThreadsafeFunction<...>>> to Option<AudioCallback> aligns with the broader refactoring effort.

packages/frontend/apps/electron/src/main/recording/feature.ts (6)

69-73: LGTM - Centralized native module access.

The getNativeModule() helper provides a single point for dynamic require of @affine/native, avoiding load on unsupported platforms while maintaining type safety.


75-95: LGTM - Improved cleanup with proper error handling.

The cleanup function now:

  1. Properly iterates both appStateSubscribers and subscribers
  2. Catches and ignores unsubscribe errors
  3. Resets arrays correctly (subscribers.length = 0 and reassignment for appStateSubscribers)
  4. Clears the observables

526-583: stopRecording should handle case where native stop fails but state already transitioned.

At line 538-541, the state machine is dispatched with STOP_RECORDING before calling the native stopRecording. If the native call fails (line 570), the state has already transitioned to "stopped" but no artifact was saved. The error handling dispatches CREATE_BLOCK_FAILED, which is appropriate, but consider whether the early state transition is intentional.


559-567: Async window focus should not block recording stop.

The getMainWindow().then(...) call is fire-and-forget, which is correct for not blocking the return. However, errors are only logged. This is acceptable behavior.


585-594: Good path traversal protection.

The assertRecordingFilepath function properly validates that the filepath is within SAVED_RECORDINGS_DIR using normalized path comparison, preventing path traversal attacks.


257-272: getRecording now reads from state machine - verify callers expect this behavior.

The function now returns data from recordingStateMachine.status instead of a Map. Callers should be aware that only the current active recording can be retrieved, not historical ones.

Comment on lines 419 to 424
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId });
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing timestamp conversion for getDocIndexedClock return value.

Same issue as the iOS implementation: the native response is returned directly without converting the timestamp field from a number to a Date object, as required by the DocIndexedClock interface.

🔎 Proposed fix
   getDocIndexedClock: function (
     id: string,
     docId: string
   ): Promise<DocIndexedClock | null> {
-    return NbStore.getDocIndexedClock({ id, docId });
+    return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
+      clock
+        ? {
+            docId: clock.docId,
+            timestamp: new Date(clock.timestamp),
+            indexerVersion: clock.indexerVersion,
+          }
+        : null
+    );
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId });
},
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
clock
? {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
indexerVersion: clock.indexerVersion,
}
: null
);
},
🤖 Prompt for AI Agents
In packages/frontend/apps/android/src/plugins/nbstore/index.ts around lines 419
to 424, the native result from NbStore.getDocIndexedClock is returned directly
without converting the numeric timestamp into a Date as required by the
DocIndexedClock interface; change the function to await the native call, check
for null, and return the object with the timestamp field converted via new
Date(numericTimestamp) (preserving other fields) so callers receive a Date
instance.

Comment on lines +118 to +127
apis?.recording
.getCurrentRecording()
.then(status => {
if (status) {
return handleRecordingStatusChanged(status);
}
return;
})
.catch(console.error);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate getCurrentRecording call may cause race condition.

The getCurrentRecording API is called twice on mount: once in the useRecordingStatus hook (line 34) and again here (line 118). This creates redundant API calls and potential race conditions since both execute concurrently.

The useRecordingStatus hook already handles the initial status by calling getCurrentRecording and setting the status state. The tracking logic in handleRecordingStatusChanged (which only processes 'new' status) could instead be triggered by the status change event subscription (line 129) or moved into useRecordingStatus.

🔎 Suggested refactor to remove duplicate call

Consider removing the duplicate getCurrentRecording call and relying on the useRecordingStatus hook and event subscription:

-    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);
       }
     });

Alternatively, if initial tracking is critical, move the tracking logic into useRecordingStatus or trigger it when status changes using a separate useEffect that depends on status.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
});
🤖 Prompt for AI Agents
In packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx around
lines 118 to 127, remove the redundant apis.recording.getCurrentRecording() call
to avoid duplicate concurrent API requests and potential race conditions;
instead rely on the existing useRecordingStatus hook (which already calls
getCurrentRecording) and the event subscription to trigger
handleRecordingStatusChanged, or move the tracking logic into useRecordingStatus
(or a separate useEffect that reacts to status changes) so the initial status is
handled only once and tracking runs when the hook's status updates.

Comment on lines +474 to 516
export async function startRecording(
appGroup?: AppGroupInfo | number
): RecordingStatus | null {
const state = recordingStateMachine.dispatch(
{
type: 'START_RECORDING',
appGroup: normalizeAppGroupInfo(appGroup),
},
false
);
): Promise<RecordingStatus | null> {
const state = recordingStateMachine.dispatch({
type: 'START_RECORDING',
appGroup: normalizeAppGroupInfo(appGroup),
});

if (state?.status === 'recording') {
createRecording(state);
if (!state || state.status !== 'recording') {
return state;
}

recordingStateMachine.status$.next(state);
try {
fs.ensureDirSync(SAVED_RECORDINGS_DIR);

const meta = getNativeModule().startRecording({
appProcessId: state.app?.processId,
outputDir: SAVED_RECORDINGS_DIR,
format: 'opus',
id: String(state.id),
});

const filepath = assertRecordingFilepath(meta.filepath);
const nextState = recordingStateMachine.dispatch({
type: 'ATTACH_NATIVE_RECORDING',
id: state.id,
nativeId: meta.id,
startTime: meta.startedAt ?? state.startTime,
filepath,
sampleRate: meta.sampleRate,
numberOfChannels: meta.channels,
});

return state;
return nextState;
} catch (error) {
logger.error('failed to start recording', error);
return recordingStateMachine.dispatch({
type: 'CREATE_BLOCK_FAILED',
id: state.id,
error: error instanceof Error ? error : undefined,
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Error in startRecording leaves capture running on ATTACH_NATIVE_RECORDING failure.

If startRecording succeeds at line 489 but the ATTACH_NATIVE_RECORDING dispatch fails or the state machine rejects it, the native recording continues running without being tracked. Consider stopping the native recording if attachment fails.

🔎 Suggested fix: Stop native recording on attachment failure
   try {
     fs.ensureDirSync(SAVED_RECORDINGS_DIR);

     const meta = getNativeModule().startRecording({
       appProcessId: state.app?.processId,
       outputDir: SAVED_RECORDINGS_DIR,
       format: 'opus',
       id: String(state.id),
     });

     const filepath = assertRecordingFilepath(meta.filepath);
     const nextState = recordingStateMachine.dispatch({
       type: 'ATTACH_NATIVE_RECORDING',
       id: state.id,
       nativeId: meta.id,
       startTime: meta.startedAt ?? state.startTime,
       filepath,
       sampleRate: meta.sampleRate,
       numberOfChannels: meta.channels,
     });

+    if (!nextState || nextState.status !== 'recording') {
+      // Attachment failed, stop the native recording to avoid orphaned capture
+      try {
+        getNativeModule().stopRecording(meta.id);
+      } catch (stopErr) {
+        logger.error('failed to stop orphaned native recording', stopErr);
+      }
+    }
+
     return nextState;
   } catch (error) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
packages/frontend/apps/electron/src/main/recording/feature.ts around lines 474
to 516: the code can start a native recording (meta from startRecording) but if
the ATTACH_NATIVE_RECORDING dispatch fails you leave the native recorder
running; fix by capturing the meta after startRecording, then wrap the
ATTACH_NATIVE_RECORDING dispatch in a try/catch (or check its return) and on any
failure call getNativeModule().stopRecording(meta.id) (and log any stop errors)
before returning the failed state; ensure meta is in scope for the catch, and
still return the recordingStateMachine dispatch for CREATE_BLOCK_FAILED with the
original error.

Comment on lines 419 to 424
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId });
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing timestamp conversion for getDocIndexedClock return value.

Per the DocIndexedClock interface in packages/frontend/native/index.d.ts (lines 140-144), the timestamp field should be a Date object. However, this implementation returns the native response directly without converting the timestamp from a number to a Date. Other clock methods like getDocClock (lines 112-126) perform this conversion.

🔎 Proposed fix
   getDocIndexedClock: function (
     id: string,
     docId: string
   ): Promise<DocIndexedClock | null> {
-    return NbStore.getDocIndexedClock({ id, docId });
+    return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
+      clock
+        ? {
+            docId: clock.docId,
+            timestamp: new Date(clock.timestamp),
+            indexerVersion: clock.indexerVersion,
+          }
+        : null
+    );
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId });
},
getDocIndexedClock: function (
id: string,
docId: string
): Promise<DocIndexedClock | null> {
return NbStore.getDocIndexedClock({ id, docId }).then(clock =>
clock
? {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
indexerVersion: clock.indexerVersion,
}
: null
);
},
🤖 Prompt for AI Agents
In packages/frontend/apps/ios/src/plugins/nbstore/index.ts around lines 419 to
424, the getDocIndexedClock implementation returns the native object directly
without converting the numeric timestamp to a Date; update the function to await
NbStore.getDocIndexedClock, return null if the native result is null, and
otherwise return an object that spreads the native result but replaces
timestamp: new Date(native.timestamp) (keeping other fields intact) so the
function returns Promise<DocIndexedClock | null> with timestamp as a Date.

Comment on lines +498 to +557
#[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());
}
}

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)?;
}

let (tx, rx) = bounded::<Vec<f32>>(32);
let (capture, capture_rate, capture_channels) =
start_capture(&opts, tx.clone()).map_err(|e| RecordingError::Start(e.to_string()))?;

let encoding_channels = match opts.channels {
Some(1) => 1,
Some(2) => 2,
_ => capture_channels,
};

let worker = spawn_worker(
id.clone(),
filepath.clone(),
rx,
capture_rate,
encoding_channels,
);

let meta = RecordingSessionMeta {
id: id.clone(),
filepath: filepath.to_string_lossy().to_string(),
sample_rate: ENCODE_SAMPLE_RATE.as_i32() as u32,
channels: encoding_channels,
started_at: now_millis(),
};

let mut 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());
}

recordings.insert(
id,
ActiveRecording {
sender: Some(tx),
capture,
worker: Some(worker),
},
);

Ok(meta)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Recording ID collision not fully prevented.

Line 543-545 checks for duplicate recording IDs, but the ID is generated from opts.id or timestamp before acquiring the lock (line 507). In a race condition, two calls with the same user-provided ID could both pass validation before either acquires the lock. Consider generating the ID inside the critical section or using a more robust ID generation strategy.

🔎 Suggested fix: Move ID generation inside the lock
 #[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());
     }
   }
 
   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)?;
-  }
 
   let (tx, rx) = bounded::<Vec<f32>>(32);
   let (capture, capture_rate, capture_channels) =
     start_capture(&opts, tx.clone()).map_err(|e| RecordingError::Start(e.to_string()))?;
 
   let encoding_channels = match opts.channels {
     Some(1) => 1,
     Some(2) => 2,
     _ => capture_channels,
   };
 
+  let mut recordings = ACTIVE_RECORDINGS
+    .lock()
+    .map_err(|_| RecordingError::Start("lock poisoned".into()))?;
+
+  let id = sanitize_id(opts.id.clone());
+  if recordings.contains_key(&id) {
+    return Err(RecordingError::Start("duplicate recording id".into()).into());
+  }
+
+  let filepath = output_dir.join(format!("{id}.opus"));
+  if filepath.exists() {
+    fs::remove_file(&filepath)?;
+  }
+
   let worker = spawn_worker(
     id.clone(),
     filepath.clone(),
     rx,
     capture_rate,
     encoding_channels,
   );
 
   let meta = RecordingSessionMeta {
     id: id.clone(),
     filepath: filepath.to_string_lossy().to_string(),
     sample_rate: ENCODE_SAMPLE_RATE.as_i32() as u32,
     channels: encoding_channels,
     started_at: now_millis(),
   };
 
-  let mut 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());
-  }
-
   recordings.insert(
     id,
     ActiveRecording {
       sender: Some(tx),
       capture,
       worker: Some(worker),
     },
   );
 
   Ok(meta)
 }
🤖 Prompt for AI Agents
In packages/frontend/native/media_capture/src/recording.rs around lines 498 to
557, the recording ID and derived filepath are created before acquiring the
ACTIVE_RECORDINGS lock, allowing a race where two callers supply the same ID and
both pass the duplicate check; move ID generation/sanitization
(sanitize_id(opts.id.clone())) and any logic that depends on that ID (filepath
construction, existence check, and removal) inside the critical section that
holds ACTIVE_RECORDINGS, so you acquire the lock first, compute/sanitize the id,
check recordings.contains_key(&id) and only then create filepath/remove existing
file and insert the ActiveRecording; adjust order so start_capture and
spawn_worker occur after the insert or ensure the worker/sender are created in a
way that won’t leak on failure, and keep error handling consistent (map lock
poisoning to RecordingError::Start).

@codecov
Copy link

codecov bot commented Dec 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.51%. Comparing base (e7d0f31) to head (2ac9158).

Additional details and impacted files
@@           Coverage Diff            @@
##           canary   #14188    +/-   ##
========================================
  Coverage   56.50%   56.51%            
========================================
  Files        2775     2775            
  Lines      142431   142433     +2     
  Branches    21696    21701     +5     
========================================
+ Hits        80483    80496    +13     
+ Misses      60165    59601   -564     
- Partials     1783     2336   +553     
Flag Coverage Δ
server-test 76.16% <ø> (+0.01%) ⬆️
unittest 32.04% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@darkskygit darkskygit force-pushed the darksky/native-sync-state branch from 5d4a3b5 to 7ef550a Compare December 31, 2025 04:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx (1)

118-127: Duplicate getCurrentRecording call still present.

This concern was raised in a previous review. The getCurrentRecording API is called twice on mount: once in useRecordingStatus (line 34) and again here (line 118). This creates redundant API calls and potential race conditions since both execute concurrently.

packages/frontend/native/media_capture/src/recording.rs (1)

498-557: Recording ID collision allows resource leak on duplicate detection.

This issue was flagged in a previous review. The current flow starts capture and spawns the worker (lines 513-529) before acquiring the lock and checking for duplicate IDs (lines 539-545). If a duplicate is detected, the capture session and worker thread are orphaned.

The suggested fix from the previous review to move ID generation and duplicate checking inside the critical section (before starting capture) would resolve this.

packages/frontend/apps/electron/src/main/recording/feature.ts (1)

486-515: Native recording may be orphaned if state machine dispatch fails.

This was flagged in a previous review. If startRecording succeeds (line 489) but the ATTACH_NATIVE_RECORDING dispatch fails or the state machine rejects it (e.g., status changed between dispatches), the native recording continues running without being tracked.

Consider stopping the native recording if attachment fails:

🔎 Suggested fix
     const nextState = recordingStateMachine.dispatch({
       type: 'ATTACH_NATIVE_RECORDING',
       id: state.id,
       nativeId: meta.id,
       startTime: meta.startedAt ?? state.startTime,
       filepath,
       sampleRate: meta.sampleRate,
       numberOfChannels: meta.channels,
     });

+    if (!nextState || nextState.status !== 'recording') {
+      try {
+        getNativeModule().stopRecording(meta.id);
+      } catch (stopErr) {
+        logger.error('failed to stop orphaned native recording', stopErr);
+      }
+    }
+
     return nextState;
🧹 Nitpick comments (3)
packages/frontend/native/media_capture/src/windows/audio_capture.rs (1)

246-248: Consider documenting the default sample rate selection logic.

The default selection of min(mic_sample_rate, lb_sample_rate) is reasonable to avoid upsampling, but a brief comment explaining this choice would help future maintainers understand the rationale.

🔎 Suggested documentation
   let mic_sample_rate = mic_config.sample_rate();
   let lb_sample_rate = lb_config.sample_rate();
+  // Default to the lower of the two rates to avoid unnecessary upsampling
   let target_rate = target_sample_rate.unwrap_or(SampleRate(mic_sample_rate.min(lb_sample_rate).0));
packages/frontend/native/media_capture/Cargo.toml (1)

18-18: Add opus-codec to workspace.dependencies for consistency.

The opus-codec dependency is pinned to a specific version "0.1.2" while all other dependencies use { workspace = true }. Align this with the project's dependency management pattern by adding opus-codec = "0.1.2" to the workspace.dependencies section in the root Cargo.toml and using { workspace = true } in this package.

packages/frontend/native/media_capture/src/recording.rs (1)

131-138: Edge case: partial interleaved frames may silently drop samples.

If interleaved ends with a partial frame (length not divisible by self.channels), the inner loop silently skips samples where idx >= self.channels. This could happen if the audio source delivers misaligned buffers.

Consider adding a debug assertion or handling the remainder explicitly.

🔎 Suggested diagnostic
 fn feed(&mut self, interleaved: &[f32]) -> Vec<f32> {
+  debug_assert!(
+    interleaved.len() % self.channels == 0,
+    "interleaved buffer length {} not divisible by channels {}",
+    interleaved.len(),
+    self.channels
+  );
   for frame in interleaved.chunks(self.channels) {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3ac5d and 7ef550a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx
  • packages/frontend/apps/electron/src/main/recording/feature.ts
  • packages/frontend/apps/electron/src/main/recording/index.ts
  • packages/frontend/apps/electron/src/main/recording/state-machine.ts
  • packages/frontend/apps/electron/src/main/recording/types.ts
  • packages/frontend/core/src/utils/opus-encoding.ts
  • packages/frontend/native/index.d.ts
  • packages/frontend/native/index.js
  • packages/frontend/native/media_capture/Cargo.toml
  • packages/frontend/native/media_capture/src/audio_callback.rs
  • packages/frontend/native/media_capture/src/lib.rs
  • packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs
  • packages/frontend/native/media_capture/src/macos/tap_audio.rs
  • packages/frontend/native/media_capture/src/recording.rs
  • packages/frontend/native/media_capture/src/windows/audio_capture.rs
  • packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs
💤 Files with no reviewable changes (2)
  • packages/frontend/core/src/utils/opus-encoding.ts
  • packages/frontend/apps/electron/src/main/recording/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/frontend/native/media_capture/src/lib.rs
  • Cargo.toml
  • packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs
  • packages/frontend/apps/electron/src/main/recording/state-machine.ts
  • packages/frontend/native/index.d.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Use TypeScript as the programming language

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Expose JavaScript APIs to native iOS code through window object for Capacitor integration

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Enable TypeScript strict mode

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getAiButtonFeatureFlag() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentI18nLocale() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/types.ts
📚 Learning: 2025-11-27T03:23:11.880Z
Learnt from: CR
Repo: toeverything/AFFiNE PR: 0
File: packages/frontend/apps/ios/AGENTS.md:0-0
Timestamp: 2025-11-27T03:23:11.880Z
Learning: Applies to packages/frontend/apps/ios/**/*.{ts,tsx} : Implement getCurrentDocContentInMarkdown() API for native iOS bridge

Applied to files:

  • packages/frontend/apps/electron/src/main/recording/feature.ts
🧬 Code graph analysis (6)
packages/frontend/native/index.js (1)
packages/common/y-octo/node/index.js (1)
  • nativeBinding (7-7)
packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx (2)
packages/frontend/apps/electron/src/preload/electron-api.ts (1)
  • apis (278-281)
packages/frontend/apps/electron/src/main/recording/state-machine.ts (1)
  • status (61-63)
packages/frontend/native/media_capture/src/recording.rs (1)
packages/frontend/native/index.d.ts (3)
  • RecordingStartOptions (61-69)
  • RecordingSessionMeta (53-59)
  • RecordingArtifact (44-51)
packages/frontend/native/media_capture/src/macos/tap_audio.rs (1)
packages/frontend/native/media_capture/src/recording.rs (1)
  • start_capture (434-478)
packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs (2)
packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs (4)
  • tap_audio_with_callback (681-709)
  • tap_audio (712-720)
  • new (104-110)
  • new (529-533)
packages/frontend/native/media_capture/src/windows/audio_capture.rs (2)
  • start_recording (220-395)
  • new (32-49)
packages/frontend/apps/electron/src/main/recording/feature.ts (3)
packages/frontend/apps/electron/src/main/recording/state-machine.ts (2)
  • recordingStateMachine (356-356)
  • status (61-63)
packages/frontend/apps/electron/src/main/recording/types.ts (2)
  • AppGroupInfo (12-19)
  • RecordingStatus (21-46)
packages/frontend/apps/electron/src/main/windows-manager/main-window.ts (1)
  • getMainWindow (251-253)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: y-octo binding test on aarch64-pc-windows-msvc
  • GitHub Check: y-octo binding test on x86_64-apple-darwin
  • GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
  • GitHub Check: Build AFFiNE native (x86_64-apple-darwin)
  • GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
  • GitHub Check: Lint
🔇 Additional comments (31)
packages/frontend/native/media_capture/src/audio_callback.rs (1)

1-29: Clean abstraction for dual-target audio delivery.

The AudioCallback enum provides a clean abstraction for routing audio to either JS or native channels. The non-blocking semantics with intentional error suppression are appropriate for real-time audio processing where blocking would cause worse problems than occasional sample drops.

One consideration: for debugging purposes, you might want to track dropped samples via a counter or occasional log, especially during development.

packages/frontend/apps/electron-renderer/src/popup/recording/index.tsx (1)

103-139: Effect cleanup pattern is correct.

The removed flag pattern properly prevents state updates after unmount. The empty dependency array is appropriate since this effect should only run once on component mount.

packages/frontend/native/index.js (1)

582-583: New recording API exports added correctly.

The startRecording and stopRecording exports are properly wired to the native binding, consistent with the pattern used for other exports in this auto-generated file.

packages/frontend/native/media_capture/src/windows/audio_capture.rs (2)

220-223: Function signature updated correctly for AudioCallback abstraction.

The updated signature accepting AudioCallback and optional target_sample_rate aligns with the cross-platform abstraction introduced in this PR. The parameter ordering is consistent with the macOS implementation.


346-350: Callback invocation simplified correctly.

The change from func.call(Ok(mixed.clone().into()), ThreadsafeFunctionCallMode::NonBlocking) to audio_buffer_callback.call(mixed) is cleaner and delegates the complexity to the AudioCallback abstraction.

packages/frontend/native/media_capture/Cargo.toml (1)

13-22: Dependency additions support the new recording architecture.

The new dependencies (crossbeam-channel, ogg, opus-codec, rand, thiserror) align with the PR's goal of moving Opus encoding to the native layer. Moving crossbeam-channel from Windows-specific to global dependencies makes sense given the new AudioCallback::Channel variant used across platforms.

packages/frontend/native/media_capture/src/windows/screen_capture_kit.rs (3)

235-242: New callback-based entry point added correctly.

The tap_audio_with_callback function properly converts Option<u32> to Option<SampleRate> and delegates to the underlying start_recording. This mirrors the pattern used in the macOS implementation for cross-platform consistency.


244-256: Clean delegation to callback-based implementation.

The public tap_audio NAPI function now correctly delegates to tap_audio_with_callback, wrapping the ThreadsafeFunction in AudioCallback::Js(Arc::new(...)). This maintains backward compatibility while enabling the new callback abstraction.


258-267: Global audio tap follows same pattern.

The tap_global_audio_with_callback mirrors tap_audio_with_callback correctly. Note that _excluded_processes is unused on Windows since per-application audio tapping isn't supported—the comment in tap_audio at line 249-250 explains this platform limitation.

packages/frontend/apps/electron/src/main/recording/types.ts (2)

1-1: Import cleanup reflects architectural shift.

The removal of WriteStream and AudioCaptureSession imports aligns with the PR's goal of moving recording lifecycle management to the native layer. The type now correctly depends only on ApplicationInfo from the native module.


43-45: New metadata fields support native recording integration.

The addition of nativeId, sampleRate, and numberOfChannels as optional fields provides the necessary metadata for the native recording flow while maintaining backward compatibility with existing code.

packages/frontend/native/media_capture/src/macos/tap_audio.rs (4)

240-245: AggregateDevice::start updated for AudioCallback abstraction.

The signature change accepting AudioCallback instead of Arc<ThreadsafeFunction<...>> aligns with the cross-platform abstraction. The original_audio_stats parameter is retained for consistent target rate handling during device switches.


301-302: Callback invocation simplified and comment updated.

The change from audio_stream_callback.call(Ok(mixed_samples.into()), ThreadsafeFunctionCallMode::NonBlocking) to audio_stream_callback.call(mixed_samples) properly delegates to the AudioCallback abstraction. The comment change from "JavaScript sink" to "configured sink" accurately reflects the new flexibility.


612-633: start_capture correctly stores and uses AudioCallback.

The method properly:

  1. Stores the callback for device switch scenarios (line 614)
  2. Clones it for the initial stream start (line 633)
  3. Makes it available in the device change listener closure (line 661)

This ensures the callback remains valid throughout the capture lifecycle.


674-736: Device change listener correctly handles AudioCallback.

The listener closure captures callback_arc (a clone of AudioCallback) and uses it when recreating the stream on device changes (line 722). Since AudioCallback derives Clone and contains Arc or Sender (both thread-safe), this usage is correct.

packages/frontend/native/media_capture/src/recording.rs (10)

1-27: LGTM!

Imports and constants are well-organized. The 48kHz sample rate is standard for Opus, and the 4096-byte max packet size provides adequate headroom for encoded frames.


30-58: LGTM!

The N-API structs are well-defined and align with the TypeScript interfaces in index.d.ts. Field types are appropriate for cross-language interop.


60-102: LGTM!

Comprehensive error handling with clear error codes and proper conversion to N-API errors. The thiserror derive keeps the implementation clean.


170-230: LGTM!

The OggOpusWriter implementation is well-structured. The encoder initialization with 20ms frames at 48kHz is appropriate for Opus, and the optional resampler integration is clean.


279-321: LGTM!

The finish() method correctly handles partial frames by zero-padding, properly writes the end-of-stream marker, and accurately calculates duration from samples written. Good defensive cleanup of empty recordings.


387-417: LGTM!

Helper functions are well-implemented. The validate_output_dir correctly requires absolute paths and canonicalizes to prevent path traversal. The sanitize_id provides safe filename generation.


434-478: LGTM!

Platform-specific capture initialization is cleanly abstracted. The sample rate clamping and channel extraction handle edge cases appropriately.


480-496: LGTM!

The worker thread implementation is clean and straightforward. The channel-based iteration naturally terminates when the sender is dropped.


559-581: LGTM!

The stop sequence correctly stops the capture first, then signals the worker by dropping the sender, and waits for the worker to finish. The error handling properly propagates both join failures and encoding errors.

Minor: Line 563 reuses RecordingError::Start for lock poisoning in stop context; a dedicated variant might improve clarity, but this is a minor nit.


363-363: Document or remove the unsafe Send implementation for PlatformCapture.

The unsafe impl Send for PlatformCapture at line 363 lacks documentation justifying thread-safety. The macOS variant contains a raw pointer (Option<*mut c_void>) managed through CoreAudio C APIs, which does not automatically implement Send. Either add explicit documentation of the thread-safety guarantees and constraints under which this is safe, or derive Send/Sync bounds conditionally on the platform-specific types.

packages/frontend/apps/electron/src/main/recording/feature.ts (6)

69-73: LGTM!

The getNativeModule() accessor pattern correctly handles dynamic loading of the native module to avoid loading it on unsupported platforms. The type alias provides proper type safety.


75-95: LGTM!

Cleanup function properly resets all state including the new appStateSubscribers array. Silencing unsubscribe errors during cleanup is appropriate.


257-272: LGTM!

The getRecording function now correctly derives recording metadata from the state machine status, aligning with the native module integration.


526-583: LGTM!

The stopRecording function properly coordinates native module cleanup with state machine transitions. The error handling correctly falls back to CREATE_BLOCK_FAILED state, and the async window show doesn't block the return.


633-647: LGTM!

The serialization now includes the new native recording metadata fields (filepath, sampleRate, numberOfChannels) for IPC communication.


109-132: LGTM!

The createAppGroup function correctly uses getNativeModule().ShareableContent for platform-safe access to the native API.

Comment on lines +329 to +336
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(&(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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Opus encoder delay pre-skip sample rate 48kHz RFC 7845 Ogg container specification

💡 Result:

Summary — Opus pre-skip in Ogg (RFC 7845)

  • The OpusHead "pre-skip" field is a 16-bit unsigned little-endian count of samples at 48 kHz that MUST be discarded (decoded but not played) at stream start. It is used to remove encoder algorithmic delay from playback. [1] (datatracker.ietf.org)
  • PCM sample position = granule_position − pre-skip; playback time = (PCM sample position) / 48000.0. RFC 7845 gives the example and the 48 kHz basis. [1] (datatracker.ietf.org)
  • For cropping, RFC 7845 RECOMMENDS a pre-skip of at least 3,840 samples (80 ms) to ensure decoder convergence. [1] (datatracker.ietf.org)
  • Encoders SHOULD set pre-skip = encoder lookahead + any extra LPC padding used. Reference-implementation encoders expose the lookahead via OPUS_GET_LOOKAHEAD (opus_encoder_ctl). [1][2] (datatracker.ietf.org)

Sources
[1] RFC 7845 — Ogg Encapsulation for the Opus Audio Codec. (datatracker.ietf.org)
[2] Opus API (opus_encoder_ctl / OPUS_GET_LOOKAHEAD). (opus-codec.org)

Citations:


🌐 Web query:

libopus OPUS_GET_LOOKAHEAD samples 48kHz encoder delay

💡 Result:

OPUS_GET_LOOKAHEAD returns the total number of samples of algorithmic delay (lookahead + decoder-side delay) that the codec adds. The value is implementation- and config-dependent, so you should query it via opus_encoder_ctl(..., OPUS_GET_LOOKAHEAD, &n) rather than hard-coding a number. [1]

For the reference implementation with the normal (default) configuration at a 48 kHz decode rate the typical total algorithmic delay is 26.5 ms, which equals 26.5 ms * 48 000 = 1272 samples. Note this is a typical/default figure (the encoder part can vary with mode, version, and settings). [2][1]

Sources:

  • libopus encoder CTLs (OPUS_GET_LOOKAHEAD). [1]
  • RFC 6716 / Opus algorithmic delay (default 26.5 ms → 1272 samples at 48 kHz). [2]

🏁 Script executed:

cat -n packages/frontend/native/media_capture/src/recording.rs | head -80

Repository: toeverything/AFFiNE

Length of output: 2681


🏁 Script executed:

sed -n '300,360p' packages/frontend/native/media_capture/src/recording.rs

Repository: toeverything/AFFiNE

Length of output: 2203


🏁 Script executed:

sed -n '1,100p' packages/frontend/native/media_capture/src/recording.rs | grep -A 20 "Encoder::"

Repository: toeverything/AFFiNE

Length of output: 45


🏁 Script executed:

grep -n "Encoder::" packages/frontend/native/media_capture/src/recording.rs

Repository: toeverything/AFFiNE

Length of output: 140


🏁 Script executed:

grep -n "encoder" packages/frontend/native/media_capture/src/recording.rs | head -20

Repository: toeverything/AFFiNE

Length of output: 202


🏁 Script executed:

sed -n '200,240p' packages/frontend/native/media_capture/src/recording.rs

Repository: toeverything/AFFiNE

Length of output: 1110


🏁 Script executed:

grep -n "write_opus_headers" packages/frontend/native/media_capture/src/recording.rs

Repository: toeverything/AFFiNE

Length of output: 169


🏁 Script executed:

find packages/frontend/native -name "Cargo.toml" -exec grep -l "opus" {} \;

Repository: toeverything/AFFiNE

Length of output: 112


🏁 Script executed:

grep -A2 "opus" packages/frontend/native/media_capture/Cargo.toml

Repository: toeverything/AFFiNE

Length of output: 172


🌐 Web query:

opus-codec 0.1.2 Rust crate lookahead OPUS_GET_LOOKAHEAD encoder delay

💡 Result:

Short answer — what it is and how to get it

  • The opus-codec 0.1.2 crate bundles libopus (tag v1.5.2) and exposes the underlying Opus CTL to query lookahead. [1][2]
  • OPUS_GET_LOOKAHEAD returns the total number of samples of algorithmic delay added by the codec (encoder+decoder). Call the encoder CTL (opus_encoder_ctl with OPUS_GET_LOOKAHEAD) to get the value; the encoder-side contribution can vary by implementation, version, and configuration, so query it at runtime rather than hard-coding a value. [2]
  • RFC guidance: use that lookahead value (plus any extra LPC pre-skip you add) as pre-skip when muxing/setting granule positions so decoded output is time-aligned. [3]

Sources

  • opus-codec crate docs (v0.1.2, bundles libopus v1.5.2). [1]
  • Opus API — OPUS_GET_LOOKAHEAD / encoder CTLs. [2]
  • RFC 7845 (Opus Ogg/encapsulation) — encoder delay / pre-skip guidance. [3]

The pre-skip value should match the encoder's lookahead to comply with RFC 7845.

The OpusHead pre-skip is hardcoded to 0, but RFC 7845 states encoders SHOULD set pre-skip to the encoder's lookahead (plus any extra padding). For libopus at 48 kHz, the typical algorithmic delay is approximately 1272 samples (26.5 ms). The opus-codec crate exposes the OPUS_GET_LOOKAHEAD CTL to query this value at runtime.

Currently, write_opus_headers() (line 211) is called before the encoder is created (line 214), preventing access to the lookahead value. To fix this, either refactor write_opus_headers() to accept the encoder as a parameter, or move the header write after encoder initialization and query the lookahead via opus_encoder_ctl.

Without this, audio playback may have timing issues at the stream start on standards-compliant decoders.

🤖 Prompt for AI Agents
In packages/frontend/native/media_capture/src/recording.rs around lines 329-336,
the OpusHead pre-skip is hardcoded to 0 but must reflect the encoder's lookahead
per RFC 7845; change the code so the pre-skip written into the OpusHead equals
the encoder's lookahead (plus any padding) obtained via the opus-codec CTL
OPUS_GET_LOOKAHEAD. To do this either refactor write_opus_headers to accept a
reference to the created encoder or move the header write to after the encoder
is initialized (the comment indicates write_opus_headers() is called before
encoder creation at ~lines 211/214), then call the appropriate
opus_encoder_ctl/opus-codec wrapper to get the lookahead, convert to u16
little-endian and write that into the pre-skip bytes instead of 0.

@darkskygit darkskygit changed the title feat: native sync state & record encoding feat: record encoding Jan 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants