Skip to content

fix: add recording foreground service and fix call interruption, stop record when kill app or crash app#758

Open
hongdtt-ominext wants to merge 5 commits intohyochan:mainfrom
hongdtt-ominext:conver-m4a-into-wav
Open

fix: add recording foreground service and fix call interruption, stop record when kill app or crash app#758
hongdtt-ominext wants to merge 5 commits intohyochan:mainfrom
hongdtt-ominext:conver-m4a-into-wav

Conversation

@hongdtt-ominext
Copy link
Contributor

@hongdtt-ominext hongdtt-ominext commented Feb 13, 2026

Features & Fixes

  1. Foreground Service for Background Recording (Android)
  • File: RecordingForegroundService.kt (NEW)
  • Implemented Android Foreground Service to keep recording when screen is off
  • WakeLock to prevent CPU sleep during recording
  • Notification showing "Recording in progress"
  • onTaskRemoved() handler to finalize file when user swipe-kills app
  • finalizeRecordingOnKill() to properly close MediaRecorder and save file
  1. Call Interruption Handling (Both Platforms)
  • iOS (Sound.swift): Added AVAudioSession.interruptionNotification observer

    • Stops recording and saves file when phone call arrives
    • Notifies JS layer with isRecording: false and current position
    • Handles applicationWillTerminate to finalize recording on app kill
  • Android (Sound.kt): Added AudioFocusRequest handling

    • Pauses recording on audio focus loss (phone call, video, Google Assistant)
    • Handles AUDIOFOCUS_LOSS, AUDIOFOCUS_LOSS_TRANSIENT, AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
    • Uses modern AudioFocusRequest API for Android 8.0+
    • Falls back to deprecated API for older Android versions
    • setWillPauseWhenDucked(true) to pause even on "duck" requests
  1. Crash-Resilient WAV Recording (Both Platforms)
  • Android (WavRecorder.kt - NEW): Full AudioRecord-based WAV recorder

    • Records raw PCM data directly to disk
    • WAV header written at start (crash-safe)
    • repairWavFile() static method to fix headers after crash
    • finalizeOnKill() for app termination scenarios
    • Metering support (amplitude/dB)
    • Pause/resume support
  • iOS (Sound.swift): WAV recording via AVAudioEngine / AVAudioRecorder with LPCM format

  1. WAV to M4A Converter (Both Platforms)
  • Android (WavToM4aConverter.kt - NEW): Converts WAV to M4A using MediaCodec + MediaMuxer
  • iOS (WavToM4aConverter.swift - NEW): Converts WAV to M4A using AVAssetWriter
  • Both delete original WAV after successful conversion
  1. Restore/Recovery Mechanism (Both Platforms)
  • API (Sound.nitro.ts):
    • restorePendingRecordings(directory?) - Scans for incomplete WAV files, repairs headers, converts to M4A
    • restoreRecording(wavFilePath) - Restores a single WAV file
    • RestoredRecording interface: { uri, duration, originalPath }
  • Android (Sound.kt): Full implementation with WavRecorder.repairWavFile() + WavToM4aConverter.convert()
  • iOS (Sound.swift): Full implementation with WavToM4aConverter.convertSync()
  • Web (index.web.tsx): Stub implementation (not applicable for web)
  1. Audio Focus Bug Fix (Android) - This Session
  • Fixed callback returning currentPosition: 0.0 when recording pauses due to audio focus loss
  • Added getCurrentRecordingTime() to RecordingForegroundService
  • Now returns actual elapsed time before pausing
  1. Audio Focus for Video/Audio Apps (Android) - This Session
  • Upgraded from deprecated requestAudioFocus() to AudioFocusRequest API (Android 8.0+)
  • Added AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK case to handle video players properly
  • Set setWillPauseWhenDucked(true) and AUDIOFOCUS_GAIN for exclusive audio focus
  • Updated releaseAudioFocus() to use abandonAudioFocusRequest() for API 26+
  • Documentation - This Session
  1. docs/CALL_INTERRUPTION_ANALYSIS.md (English)
  • Complete interruption handling guide
  • All scenarios with status table
  • iOS and Android implementation details
  • Restore mechanism with code examples
  • FAQ section
  1. docs/ANDROID_RECORDING_STATE_FIX.md (English)
  • Recording state preservation fix documentation
  • Root cause analysis and solution
  • Testing scenarios
  1. docs/M4A_DATA_LOSS_EXPLAINED.md (English)
  • M4A file structure explanation
  • Why data is lost on crash/power off
  • WAV recovery architecture
  • API Reference for restore methods

Summary by CodeRabbit

  • New Features

    • Persistent foreground recording on Android with improved background stability
    • Crash-resilient WAV recording with automatic WAV→M4A conversion and restoration APIs (Android, iOS; web returns stubs)
  • Improvements

    • Robust interruption and audio-focus handling (calls, app termination)
    • Better metering, timing, and recovery workflows for reliable recording
  • Documentation

    • Added guides on recording state, interruption analysis, and M4A/WAV data-loss and recovery

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Adds platform-wide crash-resilient WAV recording with Android foreground-service recording, a WavRecorder, WAV→M4A conversion utilities, recovery APIs (restorePendingRecordings, restoreRecording), audio-focus and lifecycle handling, and supporting documentation.

Changes

Cohort / File(s) Summary
Android Manifest & Permissions
android/src/main/AndroidManifest.xml
Added FOREGROUND_SERVICE, FOREGROUND_SERVICE_MICROPHONE, WAKE_LOCK permissions and declared com.margelo.nitro.audiorecorderplayer.RecordingForegroundService with foregroundServiceType="microphone".
Android Foreground Service
android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt
New foreground service providing start/pause/resume/stop APIs, binder for binding, wake-lock management, notification channel, periodic progress/metering callbacks, and lifecycle handling including onTaskRemoved.
Android Recorder & Converter
android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt, .../WavToM4aConverter.kt
Added WavRecorder (AudioRecord-based WAV writer with pause/resume, metering, crash-repair, finalize-on-kill) and WavToM4aConverter (sync + suspend WAV→M4A conversion with header parsing and validation).
Android Sound Integration
android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt
Refactored to use RecordingForegroundService and WavRecorder; added ServiceConnection binding, audio-focus handling, pending-recording coordination, WAV→M4A conversion flow, recovery helpers, and cleanup/error paths.
iOS Sound & Converter
ios/Sound.swift, ios/WavToM4aConverter.swift
Switched recording defaults to Linear PCM (WAV), added background WAV→M4A conversion and restore APIs, improved interruption/lifecycle handling, memory-safe timers/meters, and converter utility using AVAssetReader/Writer.
Cross-platform Types & Web Impl
src/specs/Sound.nitro.ts, src/index.web.tsx
Added RestoredRecording type and restorePendingRecordings / restoreRecording to spec; web impl returns empty array or throws (unsupported).
Docs
docs/ANDROID_RECORDING_STATE_FIX.md, docs/CALL_INTERRUPTION_ANALYSIS.md, docs/M4A_DATA_LOSS_EXPLAINED.md
New documentation describing Android recording-state fixes, interruption handling, data-loss tradeoffs, and recovery workflows.
Package
package.json
Added devDependency baseline-browser-mapping: ^2.9.11.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Client
    participant FG as RecordingForegroundService
    participant Wav as WavRecorder
    participant Timer as Progress Timer

    App->>FG: startRecording(params)
    activate FG
    FG->>Wav: startRecording(path, config)
    activate Wav
    Wav-->>FG: started
    FG->>Timer: start periodic updates
    activate Timer
    loop every subscriptionDuration
        Timer->>Wav: getMaxAmplitude(), getCurrentDuration()
        Wav-->>Timer: amplitude, duration
        Timer->>App: onRecordingUpdate(amplitude,duration)
    end
    App->>FG: pauseRecording()
    FG->>Wav: pauseRecording()
    App->>FG: resumeRecording()
    FG->>Wav: resumeRecording()
    App->>FG: stopRecording()
    FG->>Wav: stopRecording()
    Wav-->>FG: wavFilePath
    FG->>App: wavFilePath / converted M4A
    deactivate Timer
    deactivate Wav
    deactivate FG
Loading
sequenceDiagram
    participant Hybrid as HybridSound
    participant FS as File System
    participant Wav as WavRecorder (repair)
    participant Conv as WavToM4aConverter
    participant Codec as MediaCodec/AVAssetWriter

    Hybrid->>FS: locate .wav files (restorePendingRecordings)
    loop per wav file
        Hybrid->>Wav: repairWavFile(wavPath)
        Wav-->>Hybrid: repaired
        Hybrid->>Conv: convert(wavPath)
        activate Conv
        Conv->>Codec: encode PCM→AAC + mux
        Codec-->>Conv: finished
        Conv->>FS: validate output & (optionally) delete wav
        Conv-->>Hybrid: Success(path,duration) / Error
    end
    Hybrid-->>Caller: Array<RestoredRecording>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

🛠 bugfix, 🤖 android, 📱 iOS, 🍗 enhancement

Suggested reviewers

  • hyochan

Poem

🐇 I thumped my foot and twitched my ear,

WAVs now brave what calls once fear,
A service hums to guard each note,
Converters knit the final coat,
Hooray — recordings keep their cheer! 🎧

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding a recording foreground service for Android, fixing call interruption handling, and ensuring recordings stop on app kill/crash.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @hongdtt-ominext, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly upgrades the audio recording functionality, making it far more resilient to interruptions and unexpected app terminations across both Android and iOS. By introducing a crash-resilient WAV recording mode, robust audio focus management, and a comprehensive recovery mechanism, the changes ensure that users' recordings are preserved and can be restored even in challenging scenarios. The update also includes detailed documentation to guide developers in leveraging these new capabilities for a more reliable recording experience.

Highlights

  • Foreground Service for Background Recording (Android): Implemented an Android Foreground Service with WakeLock and notification to ensure audio recording continues reliably when the screen is off, the app is in the background, or even when the user swipe-kills the application.
  • Robust Call Interruption Handling (Both Platforms): Enhanced interruption handling: iOS now stops recording and saves the file upon AVAudioSession interruptions (like phone calls), while Android intelligently pauses recording using the modern AudioFocusRequest API when audio focus is lost to other apps or calls.
  • Crash-Resilient WAV Recording (Both Platforms): Introduced a new WAV recording mode that writes raw PCM data directly to disk with the header at the beginning. This ensures that recordings are largely recoverable even if the app crashes or the device loses power unexpectedly.
  • WAV to M4A Conversion (Both Platforms): Added dedicated converters for both Android (MediaCodec + MediaMuxer) and iOS (AVAssetWriter) to efficiently convert the crash-resilient WAV files to smaller M4A format after successful recording, with an option to delete the original WAV.
  • Comprehensive Restore/Recovery Mechanism: Exposed new API methods (restorePendingRecordings, restoreRecording) that allow scanning for, repairing, and converting incomplete WAV files to M4A, providing a robust recovery solution for interrupted recordings.
  • Android Audio Focus Bug Fixes: Resolved a bug where currentPosition would incorrectly return 0.0 when recording paused due to audio focus loss, now providing accurate elapsed time. Also upgraded audio focus management to use the AudioFocusRequest API for better handling of various audio-consuming applications.
  • Detailed Documentation Updates: Added extensive documentation, including CALL_INTERRUPTION_ANALYSIS.md, ANDROID_RECORDING_STATE_FIX.md, and M4A_DATA_LOSS_EXPLAINED.md, to clearly explain the new features, their implementation, and best practices for handling audio recording interruptions and recovery.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • android/src/main/AndroidManifest.xml
    • Added FOREGROUND_SERVICE, FOREGROUND_SERVICE_MICROPHONE, and WAKE_LOCK permissions.
    • Declared RecordingForegroundService in the application manifest.
  • android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt
    • Introduced a new Android Foreground Service to manage WAV recording in the background, including wake lock management, notifications, and crash-resilient finalization.
  • android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt
    • Refactored Android audio recording logic to delegate to RecordingForegroundService.
    • Implemented modern AudioFocusRequest for robust call and audio interruption handling.
    • Added methods for restoring pending and single WAV recordings, utilizing WavRecorder and WavToM4aConverter.
  • android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt
    • Added a new class for AudioRecord-based WAV recording, supporting crash-resilient header writing, pausing, resuming, and metering.
  • android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavToM4aConverter.kt
    • Added a new Kotlin utility class to convert WAV audio files to M4A format using MediaCodec and MediaMuxer, including WAV header parsing and validation.
  • docs/ANDROID_RECORDING_STATE_FIX.md
    • Added new documentation detailing the problem, root cause, and solution for preserving Android recording state across interruptions.
  • docs/CALL_INTERRUPTION_ANALYSIS.md
    • Added comprehensive documentation analyzing call interruption scenarios, library handling mechanisms, and usage guides for both iOS and Android.
  • docs/M4A_DATA_LOSS_EXPLAINED.md
    • Added new documentation explaining M4A file structure, reasons for data loss, and the benefits of WAV as a crash-resilient alternative.
  • ios/Sound.swift
    • Modified recording to default to WAV format for crash resilience.
    • Implemented AVAudioSession.interruptionNotification observer to stop recording on interruptions.
    • Added restorePendingRecordings and restoreRecording methods for iOS, utilizing WavToM4aConverter.
    • Improved deinit logic for better resource cleanup.
    • Optimized HTTP streaming playback to use temporary files for memory efficiency.
  • ios/WavToM4aConverter.swift
    • Added a new Swift class to convert WAV files to M4A format using AVAssetReader and AVAssetWriter.
  • nitrogen/generated/ios/NitroSound-Swift-Cxx-Bridge.cpp
    • Updated generated C++ bridge code to use NON_NULL macro for pointer annotations.
  • nitrogen/generated/ios/NitroSound-Swift-Cxx-Bridge.hpp
    • Updated generated C++ bridge header to use NON_NULL macro for pointer annotations.
  • package.json
    • Updated package configuration to mark it as private and added a new development dependency.
  • src/index.web.tsx
    • Added stub implementations for recording recovery methods, indicating they are not supported on the web platform.
  • src/specs/Sound.nitro.ts
    • Defined RestoredRecording interface and added restorePendingRecordings and restoreRecording methods to the Sound interface.
Activity
  • No human activity (comments, reviews, progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In
`@android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt`:
- Around line 388-400: The acquireWakeLock() method creates a PARTIAL_WAKE_LOCK
with a 10-hour timeout (wakeLock, WAKE_LOCK_TAG) which is too long; change to a
shorter timeout (e.g., 30–60 minutes) and implement renewal while recording is
active and prompt release when recording stops. Update acquireWakeLock() to use
a reduced timeout constant, add logic (e.g., a renewWakeLock() helper or a
scheduled Runnable) to re-acquire or extend the lock periodically during active
recording, and ensure the lock is released immediately in the recording stop
path and in onDestroy() if still held; reference the wakeLock variable and
WAKE_LOCK_TAG to locate and modify the code.
- Around line 230-244: resumeRecording() currently calls startRecordTimer(60L)
which leaves the timer wrong; replace the hardcoded 60L with the stored
subscriptionDurationMs field value (convert to expected units for
startRecordTimer if necessary—for example subscriptionDurationMs / 1000 if
startRecordTimer expects seconds). Update resumeRecording() to read
subscriptionDurationMs and pass that value to startRecordTimer so the resumed
session uses the correct duration; reference wavRecorder.resumeRecording(),
startRecordTimer(...) and subscriptionDurationMs when locating the change.
- Around line 246-250: In stopRecording(), remove the redundant call to
wavRecorder?.stopRecording() and instead rely on stopRecordingInternal() to stop
and return the path (adjust stopRecordingInternal() to return the stop path if
needed) so stopRecording() simply delegates; in resumeRecording(), replace the
hardcoded startRecordTimer(60L) with the subscriptionDuration value used by
startRecording() (use the same parameter or field) so both use the same timer;
in createNotification(), null-check the result of
packageManager.getLaunchIntentForPackage(...) and provide a safe fallback
PendingIntent (e.g., create an intent to launch the current activity or use a
no-op intent) before calling PendingIntent.getActivity(); finally, reduce the
WakeLock acquire timeout from 10 * 60 * 60 * 1000L to a more reasonable duration
(e.g., 10 * 60 * 1000L) and ensure WakeLock release paths remain correct
(references: stopRecording(), stopRecordingInternal(), resumeRecording(),
startRecordTimer(), startRecording(), createNotification(),
packageManager.getLaunchIntentForPackage(), PendingIntent.getActivity(),
WakeLock.acquire()).

In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt`:
- Around line 53-71: The onRecordingUpdate callback is being assigned twice
(once in the ServiceConnection object’s onServiceConnected and again inside
startRecorder), which can race and overwrite handlers; remove the duplicate
assignment from startRecorder (the block that sets
recordingService?.onRecordingUpdate around lines ~219-230) and rely on the
ServiceConnection’s setup (serviceConnection / onServiceConnected and
RecordingForegroundService.RecordingBinder) to set onRecordingUpdate once,
ensuring any calls in startRecorder instead check isServiceBound or use the
existing recordingService without reassigning the callback.
- Around line 700-730: The duration estimation currently hardcodes 44100 Hz mono
math in the WAV fallback (around the WavRecorder.repairWavFile /
WavToM4aConverter.convert error path), which is wrong for other presets; instead
parse the WAV header to get the actual byteRate (or sampleRate, channels,
bitsPerSample) and data chunk offset and compute estimatedDurationMs =
(file.length() - dataChunkOffset) / byteRate * 1000. Update the error branch
that builds RestoredRecording to call a small helper (e.g., parseWavHeader or
getWavByteRateAndDataOffset) to extract byteRate/data offset from the WAV file
and use those values for the duration calculation so stereo/48k WAVs are
estimated correctly.
- Around line 200-276: The Thread.sleep(300) race should be removed and
recording start moved into the ServiceConnection.onServiceConnected flow:
instead of calling RecordingForegroundService.getInstance() after sleep, capture
the recording parameters and the Promise into a pending holder (e.g.,
pendingRecordingParams and pendingRecordingPromise) before binding, then in
serviceConnection.onServiceConnected (where you set recordingService and
isServiceBound) call a helper like startRecordingOnService to perform the same
start logic (setup onRecordingUpdate, call recordingService.startRecording,
resolve/reject the pending promise and handle unbind/stop on failure). Keep the
existing cleanup (context.unbindService, isServiceBound flag,
RecordingForegroundService.stop) but invoke it from the
helper/onServiceConnected paths rather than relying on getInstance() +
Thread.sleep.

In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt`:
- Around line 247-294: stopRecording() currently joins recordingThread with a 1s
timeout and then proceeds to close outputStream and release audioRecord while
the thread may still be running; change the logic so that after
recordingThread?.join(1000) you check if recordingThread?.isAlive == true and if
so call recordingThread?.interrupt() and then join again (e.g., join another
short timeout or until termination) before closing/releasing resources; ensure
the thread implementation honors interruption and checks
isRecording/Thread.currentThread().isInterrupted so it can finish any pending
writes safely; keep updateWavHeader(path, totalBytesWritten) and the final
logging only after the thread has fully terminated.
- Around line 29-53: The fields isRecording, isPaused, totalBytesWritten, and
lastMaxAmplitude are accessed from both the recording thread (recordingLoop) and
caller threads (stopRecording, pauseRecording, getMaxAmplitude) and need
cross-thread visibility; mark these fields as `@Volatile` (or otherwise
synchronize reads/writes) so updates in stopRecording/pauseRecording are seen by
the recordingLoop and getMaxAmplitude returns consistent values, ensuring
thread-safety for those variables.

In
`@android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavToM4aConverter.kt`:
- Around line 60-146: The convert and convertSync methods duplicate WAV
validation, conversion, size-checking and deletion logic; extract that shared
flow into a single private helper (e.g., private suspend fun
performConversion(wavFilePath: String, outputPath: String, bitRate: Int,
deleteWavAfterConversion: Boolean): ConversionResult) that calls parseWavHeader,
encodeToM4a, performs the "output too small" check (normalize the threshold so
both callers use the same condition), computes durationMs, handles output/input
file verification, deletes the WAV only after validation and logs/errors
consistently, and have convert and convertSync both delegate to this helper
while preserving their signatures and return types (ConversionResult) and
exception logging semantics.

In `@ios/Sound.swift`:
- Around line 1336-1360: In the .began interruption handling block where
audioRecorder is stopped, include the recorder file URL in the RecordBackType
callback so JS can access the partial recording (refer to the .began case,
audioRecorder, recordBackListener and RecordBackType); specifically, capture
recorder.url before nulling audioRecorder and add a filePath/fileUri field to
the RecordBackType payload (or populate an existing field) so the interruption
callback returns the file path; alternatively, if you prefer not to change
RecordBackType, call restorePendingRecordings() (reference
restorePendingRecordings) or otherwise surface the pending file URI to JS
immediately before setting audioRecorder = nil.
- Around line 819-823: The current duration estimate uses a hardcoded constant
(Double(fileSize - 44) / 88.2) which assumes 44.1kHz mono 16-bit and is wrong
for other recording presets; update the logic around wavPath and
estimatedDuration to read the WAV header's byteRate (4 bytes at offset 28,
little-endian) and compute duration as (fileSize - headerSize) / byteRate *
1000, falling back to a safe alternative (e.g., AVAudioFile.duration or
documenting the limitation) if the header cannot be read; ensure the change
replaces both occurrences of the hardcoded formula (the estimatedDuration
computation and the duplicate at the other occurrence) and handle file handle
errors and byteRate==0 gracefully.
- Around line 1086-1090: The current cleanup scheduled with
DispatchQueue.global(...).asyncAfter that removes tempFile after 1 second can
corrupt playback because AVAudioPlayer streams from disk; instead make the
tempFile URL an instance property (e.g., store the tempFile in the Sound class
or the player wrapper), remove the DispatchQueue.asyncAfter block, and perform
deletion in stopPlayer() (and also when the player is deinitialized or errors)
so that the file is removed only when AVAudioPlayer is stopped/released; update
stopPlayer() to check the stored tempFile URL, delete it with
FileManager.default.removeItem(at:), and nil out the stored tempFile reference.

In `@ios/WavToM4aConverter.swift`:
- Around line 85-101: The code uses deprecated synchronous AVAsset APIs
(asset.tracks(withMediaType:), audioTrack.formatDescriptions and asset.duration)
and also silently falls back to default sampleRate/channels if
CMAudioFormatDescription fails; update to use AVAsset.load(.tracks) or audio
asset.load(.tracks) and asynchronous CMAudioFormatDescription loading via
audioTrack.load(.formatDescriptions) or AVAsset.load(.duration) with await/Task
(iOS 16+), guard the loaded values and return a clear .error when
formatDescriptions or stream basic description is missing instead of defaulting
to 44100/1, and update callers of duration to await asset.load(.duration) as
well; locate these changes around AVAsset initialization, the audioTrack usage
and the extraction of sourceFormat/sampleRate/channels in
WavToM4aConverter.convert (the variables asset, audioTrack, formatDescription,
sourceFormat, sampleRate, channels).

In `@package.json`:
- Line 4: The package.json currently sets "private": true which prevents npm
publish and breaks the release-it/npm.publish workflow; remove the "private" key
(or set it to false) so the repository can be published, and verify that
publishConfig and release-it's npm.publish remain as intended; ensure no other
CI or branch-protection step relies on "private": true before merging.
🟡 Minor comments (7)
src/specs/Sound.nitro.ts-115-116 (1)

115-116: ⚠️ Potential issue | 🟡 Minor

Lint/Prettier formatting violations will fail CI.

The lint check flags formatting issues on Lines 116 and 149–152. These need to be reformatted to pass the pipeline.

Suggested fix (Line 116)
-export interface AudioSet
-  extends IOSAudioSet, AndroidAudioSet, CommonAudioSet {}
+export interface AudioSet
+  extends IOSAudioSet,
+    AndroidAudioSet,
+    CommonAudioSet {}
Suggested fix (Lines 149-152)
-export interface Sound extends HybridObject<{
-  ios: 'swift';
-  android: 'kotlin';
-}> {
+export interface Sound
+  extends HybridObject<{
+    ios: 'swift';
+    android: 'kotlin';
+  }> {

As per coding guidelines, "Use 2-space indentation, single quotes, and es5 trailing commas in TypeScript code (enforced by Prettier/ESLint)".

Also applies to: 149-152

android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt-69-95 (1)

69-95: ⚠️ Potential issue | 🟡 Minor

Integer truncation in repairWavFile for large recordings.

Lines 82 and 86 call .toInt() on Long values. For recordings exceeding ~24 minutes of stereo 44.1kHz/16-bit audio (~254 MB data), the data size exceeds Int.MAX_VALUE (2,147,483,647 bytes), producing a corrupt header. This is an inherent WAV format limitation (the RIFF size fields are 32-bit), but the truncation here is silent — the method returns true (success) even though the header is wrong.

Consider logging a warning when dataSize > Int.MAX_VALUE so callers are aware:

Suggested improvement
 val dataSize = file.length() - WAV_HEADER_SIZE
 val fileSize = dataSize + WAV_HEADER_SIZE - 8
+
+ if (dataSize > Int.MAX_VALUE) {
+     Logger.w("[WavRecorder] WAV file exceeds 2GB data limit, header may be inaccurate")
+ }
package.json-88-88 (1)

88-88: ⚠️ Potential issue | 🟡 Minor

Remove unused baseline-browser-mapping dependency.

This package is not referenced anywhere in the codebase and appears unrelated to the PR's objectives (foreground service, WAV recording, crash recovery). It should be removed from package.json.

docs/ANDROID_RECORDING_STATE_FIX.md-16-161 (1)

16-161: ⚠️ Potential issue | 🟡 Minor

Documentation code snippets reference MediaRecorder but the implementation uses WavRecorder.

The code examples throughout this document show mediaRecorder?.pause(), mediaRecorder?.resume(), mediaRecorder?.stop(), etc., but the actual RecordingForegroundService.kt delegates all recording to WavRecorder (AudioRecord-based). This mismatch will mislead contributors who read the doc and then look at the code.

Consider updating the snippets to reflect the real implementation, or add a note that these are simplified/conceptual illustrations.

android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavToM4aConverter.kt-117-117 (1)

117-117: ⚠️ Potential issue | 🟡 Minor

Division by zero if wavHeader.byteRate is 0 (corrupt or repaired WAV).

Lines 117 and 199 calculate (wavHeader.dataSize * 1000L) / wavHeader.byteRate. If the WAV file is corrupt or has a damaged header, byteRate could be 0, causing an ArithmeticException.

Proposed fix
-val durationMs = (wavHeader.dataSize * 1000L) / wavHeader.byteRate
+val durationMs = if (wavHeader.byteRate > 0) {
+    (wavHeader.dataSize * 1000L) / wavHeader.byteRate
+} else {
+    0L
+}

Also applies to: 199-199

android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt-354-378 (1)

354-378: ⚠️ Potential issue | 🟡 Minor

getLaunchIntentForPackage can return null, which would pass null to PendingIntent.getActivity.

On some devices or configurations, packageManager.getLaunchIntentForPackage(packageName) may return null. Passing it to PendingIntent.getActivity could cause unexpected behavior.

Proposed fix
     private fun createNotification(contentText: String = "Tap to return to app"): Notification {
         val packageName = packageName
         val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+            ?: Intent() // Fallback to empty intent
         val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ios/Sound.swift-1246-1263 (1)

1246-1263: ⚠️ Potential issue | 🟡 Minor

Add missing import UIKit to support UIApplication.willTerminateNotification.

UIApplication and UIApplication.willTerminateNotification are part of the UIKit framework. The file currently only imports Foundation, AVFoundation, and NitroModules but is missing the explicit UIKit import required for this code to compile. Add import UIKit at the top of the file.

🧹 Nitpick comments (13)
ios/WavToM4aConverter.swift (2)

170-196: Semaphore blocks calling thread — potential deadlock risk if called from the main queue.

convertSync uses two DispatchSemaphore.wait() calls. If this is ever called from the main thread, and the writer's callback dispatches to main, it will deadlock. The convert() wrapper at Line 39 dispatches to .userInitiated, so the typical path is safe, but convertSync is static and public, making it callable from anywhere.

Consider adding a precondition or documenting that convertSync must not be called on the main thread.


238-249: Duration validation only runs when deleteWavAfterConversion is true.

The duration integrity check (comparing output M4A duration to source WAV duration) is gated by deleteWavAfterConversion. If the caller passes false, a silently truncated M4A could be returned as success without any warning. Consider always performing the validation and logging a warning on mismatch, regardless of the deletion flag.

docs/CALL_INTERRUPTION_ANALYSIS.md (2)

146-162: Add language specifiers to fenced code blocks for diagrams.

Several diagram/ASCII-art code blocks lack a language specifier (Lines 146, 186, 213, 259), which triggers markdownlint MD040. Use ```text for these blocks to satisfy the linter and improve rendering in some Markdown processors.

Also applies to: 186-203, 213-229, 259-286


547-551: Blank line inside blockquote creates two separate blockquotes.

The blank line between the two quoted references (Line 549) splits them into separate blockquotes per Markdown spec, triggering MD028.

Suggested fix
 > "When an app is force-stopped, the entire process is killed instantly. Standard lifecycle methods such as onStop() and onDestroy() are not guaranteed to run."
 > — [Android Developer Documentation](https://developer.android.com/guide/components/activities/process-lifecycle)
-
+>
 > "The system provides no notification when an app is terminated while in a suspended state."
 > — [Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/applicationwillterminate(_:))
android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt (1)

505-525: Cleanup swallows exceptions without any logging.

Per detekt hints, Lines 512 and 519 catch and discard exceptions. While cleanup code is inherently best-effort, a debug-level log helps diagnose issues during development.

Suggested fix
         try {
             audioRecord?.stop()
             audioRecord?.release()
         } catch (e: Exception) {
-            // Ignore
+            Logger.d("[WavRecorder] Cleanup: ${e.message}")
         }
         audioRecord = null
 
         try {
             outputStream?.close()
         } catch (e: Exception) {
-            // Ignore
+            Logger.d("[WavRecorder] Cleanup: ${e.message}")
         }
docs/M4A_DATA_LOSS_EXPLAINED.md (1)

20-29: Missing blank lines around headings (MD022) and unlabeled code blocks (MD040).

Lines 20 and 26 need a blank line before the heading. Additionally, diagram code blocks (Lines 37, 90, 106, 142, 162, 196) should use ```text to satisfy markdownlint MD040.

docs/ANDROID_RECORDING_STATE_FIX.md (1)

174-193: Markdown formatting: missing blank lines around headings and fenced code blocks, and missing language specifiers.

Static analysis flagged multiple markdownlint issues in the testing scenarios section (and earlier headings). Headings need surrounding blank lines (MD022), fenced code blocks need surrounding blank lines (MD031), and code blocks should specify a language (MD040).

Example fix for one scenario block
 ### ✅ Scenario 1: Phone Call → Resume
+
-```
+```text
 Start recording → Call arrives → Pause → Call ends → Resume → OK

✅ Scenario 2: Phone Call → Stop


Apply the same pattern to all scenario blocks and headings under "Important Notes".
</details>

</blockquote></details>
<details>
<summary>android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt (2)</summary><blockquote>

`61-78`: **Static singleton `instance` pattern is fragile for an Android `Service`.**

Storing the service instance in a `companion object` and accessing it via `getInstance()` is a common but fragile pattern. If the system recreates the service (e.g., after a low-memory kill with `START_STICKY`), there's a window where `getInstance()` returns `null` while the service is being recreated. The `Sound.kt` code relies on `Thread.sleep(300)` + `getInstance()` to work around this, which is inherently racy. Prefer relying solely on the `ServiceConnection` binding for communication.

---

`85-95`: **WakeLock is acquired in `onCreate` regardless of whether recording actually starts.**

The service acquires a wake lock immediately in `onCreate`, but recording may not start successfully. Consider acquiring the wake lock only when recording begins and releasing it when recording stops, rather than tying it to the service lifecycle.

</blockquote></details>
<details>
<summary>ios/Sound.swift (1)</summary><blockquote>

`429-463`: **`stopRecorder` calls `WavToM4aConverter.convertSync` with `deleteWavAfterConversion: true` — verify the WAV-to-M4A conversion is tested for edge cases.**

If the WAV file is very short (e.g., interrupted immediately), the conversion might produce a corrupt M4A. The fallback (returning WAV path) handles this, but the user-facing contract changes from M4A to WAV silently, which could break downstream consumers expecting a specific format.

</blockquote></details>
<details>
<summary>android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavToM4aConverter.kt (1)</summary><blockquote>

`437-453`: **Empty catch blocks in `finally` swallow exceptions silently — add minimal logging.**

While it's reasonable to catch and ignore cleanup exceptions in a `finally` block, completely swallowing them makes debugging harder. At minimum, use named exceptions per detekt conventions or add a brief log.


<details>
<summary>Proposed fix</summary>

```diff
         } finally {
             try {
                 inputStream?.close()
-            } catch (e: Exception) { }
+            } catch (e: Exception) {
+                Logger.w("[WavToM4a] Error closing input stream: ${e.message}")
+            }
             
             try {
                 encoder?.stop()
                 encoder?.release()
-            } catch (e: Exception) { }
+            } catch (e: Exception) {
+                Logger.w("[WavToM4a] Error releasing encoder: ${e.message}")
+            }
             
             try {
                 if (muxerStarted) {
                     muxer?.stop()
                 }
                 muxer?.release()
-            } catch (e: Exception) { }
+            } catch (e: Exception) {
+                Logger.w("[WavToM4a] Error releasing muxer: ${e.message}")
+            }
         }
android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt (2)

869-926: Requesting AUDIOFOCUS_GAIN for a recorder is unconventional and may disrupt other apps' audio.

Audio focus is a playback concept; recording apps typically don't request it. By requesting AUDIOFOCUS_GAIN, you may cause music players and other audio apps to pause unnecessarily when the user starts recording. If the goal is to detect phone call interruptions, consider using TelephonyManager/PhoneStateListener instead, or at minimum use AUDIOFOCUS_GAIN_TRANSIENT to signal temporary usage.


102-102: Unscoped CoroutineScope(Dispatchers.IO).launch — coroutines outlive the object.

Multiple methods (e.g., startRecorder, stopRecorder, restorePendingRecordings) create ad-hoc CoroutineScope instances. These coroutines are not cancelled when the HybridSound object is destroyed, which can cause callbacks on garbage-collected objects. Consider using a class-level CoroutineScope with SupervisorJob() that can be cancelled in a cleanup/dispose method.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant enhancements to audio recording functionality on both Android and iOS, primarily focusing on crash resilience and interruption handling. On Android, a new RecordingForegroundService and WavRecorder are implemented to manage background audio recording in WAV format, utilizing a WakeLock and foreground notifications to ensure continuous operation and prevent system termination. A WavToM4aConverter is added to convert these crash-resilient WAV files to smaller M4A format upon successful completion or recovery. The main HybridSound module is refactored to delegate recording tasks to this new service, incorporating audio focus management to handle interruptions like phone calls by pausing the recording. Similarly, on iOS, recording now defaults to WAV format, and interruption observers are set up to stop and finalize recordings gracefully during audio session interruptions or app termination. Both platforms gain new restorePendingRecordings and restoreRecording methods, allowing users to recover incomplete WAV recordings by repairing their headers and converting them to M4A. Documentation has been updated to explain these new features, including the rationale behind WAV for crash resilience and detailed usage guides. Review comments highlight concerns about using Thread.sleep() for service binding on Android, recommending an asynchronous, event-driven approach; potential path traversal vulnerabilities in restorePendingRecordings and restoreRecording due to direct path usage; incorrect WakeLock management leading to battery drain; synchronous network calls in setupEnginePlayer on iOS that could cause performance issues or SSRF vulnerabilities; a hardcoded timer duration in resumeRecording on Android; and redundant code and inconsistent logging practices in the WavRecorder class.

Copy link

@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

🤖 Fix all issues with AI agents
In
`@android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt`:
- Around line 94-97: onStartCommand currently returns START_STICKY which can
restart the service after process death leaving a foreground notification with
no active recorder (wavRecorder = null); change the return value to
START_NOT_STICKY in RecordingForegroundService.onStartCommand and ensure
createNotification or startForeground is only called when wavRecorder is
non-null (or guard startForeground with a check) so the service is not restarted
as a zombie and the notification accurately reflects active recording state.

In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt`:
- Around line 246-264: If startRecorder is called while the service is still
connecting, the code currently overwrites
pendingRecordingParams/pendingRecordingPromise which can orphan the first
promise; before assigning pendingRecordingParams and pendingRecordingPromise in
the else branch, check if pendingRecordingPromise is non-null and reject it (or
resolve with a clear error) so the first caller is not left hanging, then assign
the new params and promise; keep using the existing
serviceConnection/handler.post flow (RecordingForegroundService.start,
context.bindService, setupAudioFocus) and ensure you clear or replace
pendingRecordingPromise when startRecordingOnService(recordingService, params,
promise) is called in onServiceConnected.

In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt`:
- Around line 69-95: The code currently casts fileSize/dataSize to Int
(intToByteArray(fileSize.toInt()) and intToByteArray(dataSize.toInt())), which
silently overflows for WAVs >2GB; update repairWavFile (and updateWavHeader) to
write the lower 32 bits of the Long values as an unsigned little-endian 4-byte
sequence instead of casting to Int (create a helper like
longToLittleEndian4Bytes(value: Long) that produces 4 bytes from value &
0xFFFFFFFFL), use that helper when writing the RIFF/data chunk sizes, and add a
guard/log if dataSize > 0xFFFFFFFFL to surface potential truncation (or
explicitly clamp to 0xFFFFFFFFL) rather than relying on toInt().
- Around line 351-386: finalizeOnKill currently flips isRecording and
immediately closes audio resources while the recording thread may still be
writing; mark the isRecording flag as `@Volatile` to ensure visibility to the
recorder thread, then after setting isRecording = false and isPaused = false,
wait for the recording thread to finish by calling join with a short timeout
(e.g., recordingThread?.join(200) or similar) before calling
audioRecord?.stop(), audioRecord?.release(), or closing outputStream; handle
InterruptedException/InterruptedThrowable and proceed to close resources if the
join times out, preserving the existing updateWavHeader(filePath,
totalBytesWritten) call.

In `@ios/Sound.swift`:
- Around line 768-846: The iOS recovery methods accept arbitrary file paths —
add a path traversal/sandbox guard similar to Android by implementing a
validatePathSecurity(_:) helper that resolves symlinks and ensures the canonical
path starts with one of the allowed roots (documentDirectory, cachesDirectory,
NSTemporaryDirectory), then call this helper at the start of
restorePendingRecordings(directory:) and restoreRecording(wavFilePath:) to
reject any path outside those roots (reject the Promise with a RuntimeError and
return) before performing file operations; ensure you check both provided
directory strings and individual wavFilePath inputs and reuse the same
validatePathSecurity symbol for consistency.

In `@ios/WavToM4aConverter.swift`:
- Around line 146-152: The current code silently falls back to defaults when
CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) yields nil
(sourceFormat), which can corrupt output; instead, when sourceFormat is nil, log
an error with context and immediately abort the conversion by returning .error
(do not continue using sampleRate/channels defaults). Update the block around
CMAudioFormatDescriptionGetStreamBasicDescription/ sourceFormat (and any
downstream use of sampleRate/channels) to early-return .error and avoid
producing an M4A when the source format cannot be determined.
- Around line 70-74: The code sets outputPath by calling replacingOccurrences on
wavFilePath which replaces all ".wav" substrings (including directory names);
change this to replace only the file extension: if m4aFilePath is nil, build the
output path by using wavFilePath's file-extension-safe API (e.g.,
URL(fileURLWithPath:).deletingPathExtension().appendingPathExtension("m4a") or
NSString(string: wavFilePath).deletingPathExtension + ".m4a") so the directory
components are preserved; update the assignment that defines outputPath in
WavToM4aConverter.swift (references: outputPath, wavFilePath, m4aFilePath)
accordingly.
🧹 Nitpick comments (5)
android/src/main/java/com/margelo/nitro/audiorecorderplayer/RecordingForegroundService.kt (2)

280-288: Swallowed exception in cleanupRecorder loses diagnostic context.

The catch block at line 283 silently discards the exception. At minimum, log it so crash-path debugging is not hampered.

Proposed fix
     private fun cleanupRecorder() {
         try {
             wavRecorder?.stopRecording()
         } catch (e: Exception) {
-            // Ignore
+            Logger.w("[ForegroundService] Error during recorder cleanup: ${e.message}")
         }
         wavRecorder = null
         currentRecordingPath = null
     }

406-455: WakeLock renewal creates a new instance each cycle — consider re-acquiring on the same lock.

Each renewal releases the old lock, creates a brand-new PowerManager.WakeLock, and acquires it. You could simplify by calling acquire(timeout) on the existing (released) lock, but the current approach is functionally correct and the overhead is negligible. No action required.

android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt (1)

498-518: Swallowed exceptions in cleanup() lose diagnostic info.

Per detekt hints, both catch blocks discard the exception. Since cleanup() is called on error paths, logging helps diagnose initialization failures.

Proposed fix
         try {
             audioRecord?.stop()
             audioRecord?.release()
         } catch (e: Exception) {
-            // Ignore
+            Logger.w("[WavRecorder] Error during cleanup: ${e.message}")
         }
         audioRecord = null
         
         try {
             outputStream?.close()
         } catch (e: Exception) {
-            // Ignore
+            Logger.w("[WavRecorder] Error closing stream during cleanup: ${e.message}")
         }
ios/WavToM4aConverter.swift (1)

93-116: Semaphore + Task bridge for iOS 16+ async APIs is functional but carries a subtle risk.

Blocking a GCD thread with semaphore.wait() while the Task runs on the cooperative thread pool works in practice, but Apple discourages mixing GCD semaphores with Swift concurrency because of potential thread starvation under heavy load. Since this runs on .userInitiated and is short-lived, the risk is low — just flagging it for awareness.

ios/Sound.swift (1)

99-120: Forced .wav extension on user-provided URI may surprise callers expecting their original extension.

The user's .m4a (or other) extension is silently replaced with .wav. While this is necessary for the crash-resilient WAV recording strategy, the startRecorder promise resolves with the .wav path (line 252). Callers relying on the returned path should be fine, but callers comparing the passed-in URI with the resolved path may be confused. This is documented implicitly by the conversion in stopRecorder, so just noting it.

- Replace Thread.sleep(300) race condition with event-driven service
  binding via onServiceConnected callback and pending params pattern
- Add path traversal validation for restorePendingRecordings and
  restoreRecording to prevent unauthorized filesystem access
- Move WakeLock acquire/release to recording lifecycle (start/pause/
  resume/stop) instead of entire service lifetime to reduce battery drain
- Reduce WakeLock timeout from 10h to 30min with automatic renewal
- Use subscriptionDurationMs instead of hardcoded 60L in resumeRecording
- Replace e.printStackTrace() with Logger calls for consistent logging
- Remove duplicate intToByteArrayLE in WavRecorder, reuse companion
  object's intToByteArray
- Fix redundant double stopRecording() call in ForegroundService
- Null-check launchIntent in createNotification to prevent NPE
- Fix temp audio file deleted after 1s corrupting remote playback on iOS;
  defer cleanup to stopPlayer/deinit
- Read WAV header byte rate for accurate duration estimation instead of
  hardcoded 44100Hz mono 16-bit assumption (iOS + Android)
- Replace deprecated AVAsset sync APIs with async load() on iOS 16+
- Document interruption callback behavior for iOS recording
Copy link

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ios/Sound.swift (2)

507-511: ⚠️ Potential issue | 🔴 Critical

Force unwrap on user-provided URI will crash on malformed input.

URL(string: uri) returns nil for URIs with unescaped spaces or certain special characters. A file:// prefixed path with spaces (e.g., file:///path/to my file.m4a) will crash here.

Proposed fix
 if uri.hasPrefix("file://") {
     print("🎵 URI has file:// prefix")
     // Handle file:// URLs
-    url = URL(string: uri)!
+    guard let parsedURL = URL(string: uri) else {
+        promise.reject(withError: RuntimeError.error(withMessage: "Invalid file URL: \(uri)"))
+        return
+    }
+    url = parsedURL
     print("🎵 Created URL from string: \(url)")

247-252: ⚠️ Potential issue | 🟡 Minor

startRecorder resolves with WAV path, but stopRecorder returns M4A path after conversion — document this contract.

startRecorder resolves with the WAV file URL (line 252), but stopRecorder converts to M4A and deletes the WAV (lines 448-451), resolving with the M4A path. If the caller persists the path from startRecorder, it will become a dangling reference after stopRecorder completes. This asymmetry should be documented in the API or the JS layer.

🤖 Fix all issues with AI agents
In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/Sound.kt`:
- Around line 246-267: The pendingRecordingPromise can hang if
context.bindService(...) fails; after calling context.bindService(intent,
serviceConnection, Context.BIND_AUTO_CREATE) check its boolean return and if it
is false immediately reject pendingRecordingPromise with a clear error, clear
pendingRecordingParams and pendingRecordingPromise, and undo any
started/registered work (e.g., stop or not-start
RecordingForegroundService.start and teardown audio focus via setupAudioFocus
cleanup) so there are no orphaned resources; if bindService returns true keep
the existing flow so onServiceConnected will use pendingRecordingParams and call
startRecordingOnService.
- Around line 676-684: The restorePendingRecordings logic currently scans all
.wav files (wavFiles) and can accidentally pick up the WAV for an in-progress
recording; update the filter to exclude the active recording file by checking
the recorder's current recording path or recording state. Specifically, in
restorePendingRecordings (Sound.kt) when creating wavFiles, add an exclusion:
skip any file whose path equals the recorder/recording service's
currentRecordingPath (or when recorder.isRecording() && file.path ==
recorder.getCurrentRecordingPath()), or if no path accessor exists, skip files
modified within the last few seconds to avoid the active file; ensure you
reference the recorder instance (e.g., recorder.isRecording(),
recorder.getCurrentRecordingPath()) and apply that predicate to the listFiles
filter so active recordings are never processed.

In `@android/src/main/java/com/margelo/nitro/audiorecorderplayer/WavRecorder.kt`:
- Around line 517-537: The cleanup() method currently wraps audioRecord?.stop()
and audioRecord?.release() in the same try block so if stop() throws release()
never runs; change cleanup() to use separate try/catch blocks for
audioRecord?.stop() and audioRecord?.release() (mirroring stopRecording()),
ensuring release() is always attempted even if stop() fails, then set
audioRecord = null and proceed to close outputStream in its own try/catch as
already done.

In `@ios/Sound.swift`:
- Around line 794-805: The restorePendingRecordings function currently converts
and deletes every .wav in the scanDirectory, risking data loss; update its
filtering logic to only target recordings created by this library (e.g., match
the naming convention used in startRecorder such as "sound_" prefix or a
specific UUID pattern) or restrict scanning to a dedicated subdirectory for
crash-resilient recordings; ensure the wavFiles collection is built using a
filter that checks URL.lastPathComponent.hasPrefix("sound_") (or the exact
prefix/pattern you use) and leave non-matching .wav files untouched, and
document/ensure startRecorder writes to that same prefix or subdirectory and
honors deleteWavAfterConversion only for matched files.
- Around line 924-935: The validatePathSecurity function currently uses
standardizingPath which doesn't resolve symlinks and allows path traversal via
symlink; update canonicalization to use resolvingSymlinksInPath for both the
input path (canonicalPath) and when normalizing allowedDirs entries (the map
over FileManager paths and NSTemporaryDirectory()) so you compare fully resolved
paths; ensure you still handle nils with compactMap and call standardizingPath
if needed after resolvingSymlinksInPath to normalize trailing components.

In `@package.json`:
- Line 87: Remove the unused devDependency "baseline-browser-mapping" from
package.json's devDependencies section by deleting the
"baseline-browser-mapping": "^2.9.11" entry, then update the lockfile (run npm
install or yarn install) to reflect the change and ensure no other references to
baseline-browser-mapping remain in the repo.
🧹 Nitpick comments (2)
ios/Sound.swift (2)

1464-1467: Timer invalidation in deinit may execute off the main thread.

Timer.invalidate() must be called from the thread on which the timer was installed (main thread). deinit can run on any thread. The existing stopTimer(for:) helper already handles this correctly with main-thread dispatch, but deinit bypasses it.

In practice, the [weak self] captures in timer closures provide a safety net (timers self-stop when self is nil), so this is unlikely to cause issues, but it's worth aligning with the rest of the code.

Proposed fix: reuse existing helpers
     deinit {
         `#if` DEBUG
         print("🎙️ HybridSound deinit called - cleaning up resources")
         `#endif`
         
         // Remove notification observer
         removeInterruptionObserver()
         
-        // Stop and invalidate timers
-        recordTimer?.invalidate()
-        recordTimer = nil
-        playTimer?.invalidate()
-        playTimer = nil
+        // Stop and invalidate timers on the correct thread
+        stopRecordTimer()
+        stopPlayTimer()

1148-1150: Misleading comment: this is not streaming — it loads the entire resource into memory first.

Data(contentsOf: audioURL) is a synchronous, full-content download. The comment says "streaming download" but it's a bulk load followed by a write to disk. Consider updating the comment to reflect reality, and note that this will block the background thread and consume peak memory equal to the file size.

- Guard against division by zero when byteRate is 0 in WavToM4aConverter
- Fix Prettier formatting violations in Sound.nitro.ts
- Remove unused baseline-browser-mapping devDependency
- Add missing import UIKit for UIApplication.willTerminateNotification
- Add debug/warning logging to empty catch blocks in WavRecorder and
  WavToM4aConverter cleanup/finally
- Rewrite ANDROID_RECORDING_STATE_FIX.md to reference WavRecorder
  instead of MediaRecorder
- Add language specifiers to diagram code blocks in all docs (MD040)
- Fix blockquote spacing in CALL_INTERRUPTION_ANALYSIS.md (MD028)

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant