Skip to content

Conversation

martinmitrevski
Copy link
Contributor

@martinmitrevski martinmitrevski commented Aug 18, 2025

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-1076/openwav-fix-initializers-in-streamaudiorecorder.

🎯 Goal

Allow customers to override the audio recorder.

📝 Summary

Provide bullet points with the most important changes in the codebase.

🛠 Implementation

Provide a detailed description of the implementation and explain your decisions if you find them relevant.

🎨 Showcase

Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.

Before After
img img

🧪 Manual Testing Notes

Explain how this change can be tested manually, if applicable.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an audio recorder override/initialization issue to improve extensibility and reliability.
  • Changed

    • Audio recorder initialization now uses explicit dependency configuration; integration may need minor initialization updates.
  • Documentation

    • Reorganized CHANGELOG under a StreamChat section and documented the audio recorder fix.

@martinmitrevski martinmitrevski requested a review from a team as a code owner August 18, 2025 15:56
@Stream-SDK-Bot
Copy link
Collaborator

Stream-SDK-Bot commented Aug 18, 2025

SDK Size

title develop branch diff status
StreamChat 8.04 MB 8.04 MB 0 KB 🟢
StreamChatUI 4.86 MB 4.86 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator

SDK Performance

target metric benchmark branch performance status
MessageList Hitches total duration 10 ms 5.01 ms 49.9% 🔼 🟢
Duration 2.6 s 2.54 s 2.31% 🔼 🟢
Hitch time ratio 4 ms per s 1.97 ms per s 50.75% 🔼 🟢
Frame rate 75 fps 78.23 fps 4.31% 🔼 🟢
Number of hitches 1 0.4 60.0% 🔼 🟢

Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Replaced CHANGELOG upcoming entry with a StreamChat "Fixed" subsection and added a bugfix line. Modified StreamAudioRecorder initializers: introduced a public designated initializer with an optional audioSessionConfigurator parameter and added an internal designated initializer for full dependency injection; updated wiring and docs.

Changes

Cohort / File(s) Summary
Docs: Changelog
CHANGELOG.md
Replaced the "🔄 Changed" subsection with a top-level "## StreamChat" and added a "### 🐞 Fixed" entry: "Fix StreamAudioRecorder not overridable because of init method [#3783]".
Audio Recorder Init Refactor
Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift
Replaced the previous convenience init with a public designated initializer init(configuration:audioSessionConfigurator: = StreamAudioSessionConfigurator()); added an internal designated initializer accepting all dependencies for injection; initialized dependencies directly, multicastDelegate, called super.init() then setUp(); retained required no-arg convenience init and updated docs.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant StreamAudioRecorder
    participant StreamAudioSessionConfigurator

    Client->>StreamAudioRecorder: init(configuration, audioSessionConfigurator = default)
    activate StreamAudioRecorder
    StreamAudioRecorder->>StreamAudioSessionConfigurator: use/configure audio session
    StreamAudioRecorder-->>Client: instance ready (after setUp)
    deactivate StreamAudioRecorder
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix initializers in StreamAudioRecorder (IOS-1076)

Possibly related PRs

Suggested labels

🌐 SDK: StreamChat (LLC)

Suggested reviewers

  • laevandus
  • nuno-vieira

Poem

I thumped my foot, rewired my tune,
swapped a quick hop for a steadier rune.
Init now listens, dependencies set—
a carrot-coded recorder, ready to net. 🥕🎧

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch audio-recorder-init-methods

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

1 Warning
⚠️ Please be sure to complete the Contributor Checklist in the Pull Request description

Generated by 🚫 Danger

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: 0

🧹 Nitpick comments (1)
Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (1)

148-167: Public designated initializer with DI is a solid improvement. Consider defaulting configuration to .default.

Defaulting configuration makes the call site simpler and reduces reliance on the convenience init(), while keeping API source-compatible. It also clarifies the default behavior in the doc comment.

Apply this diff to the doc and signature:

@@
-    ///   - configuration: Configuration for the recorder.
+    ///   - configuration: Configuration for the recorder. Defaults to `.default`.
@@
-    public init(
-        configuration: Configuration,
+    public init(
+        configuration: Configuration = .default,
         audioSessionConfigurator: AudioSessionConfiguring = StreamAudioSessionConfigurator()
     ) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • 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 81a1d10 and 6c75969.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (1 hunks)
⏰ 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). (3)
  • GitHub Check: Build LLC + UI (Xcode 15)
  • GitHub Check: Test LLC (Debug)
  • GitHub Check: Automated Code Review
🔇 Additional comments (3)
CHANGELOG.md (1)

6-8: Changelog entry LGTM.

Accurately captures the fix. No further action needed.

Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (2)

148-167: Initialization order and dependency setup look correct.

  • All stored properties are initialized before super.init().
  • setUp() is invoked after super.init(), which is appropriate since it subscribes to app state changes.
  • Keeping the required convenience init() for AudioRecording conformance while funneling into the new designated initializer avoids breaking changes.

169-193: Internal DI initializer improves testability and flexibility.

Exposing a fully-injectable internal initializer is great for tests and internal customization without widening the public API surface. No issues spotted.

Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title develop branch diff status
StreamChat 8.04 MB 8.04 MB 0 KB 🟢
StreamChatUI 4.86 MB 4.86 MB 0 KB 🟢

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: 0

🧹 Nitpick comments (7)
Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (7)

176-193: Internal DI initializer is solid; consider depending on protocols for testability

The internal initializer cleanly supports full dependency injection. One improvement: if a protocol exists for the meter normalizer (e.g., something like AudioValueNormalising), prefer that over the concrete AudioValuePercentageNormaliser to broaden substitutability in tests/mocks. Same idea applies to the AV provider: if you have a typealias (e.g., typealias AudioRecorderAVProvider = (URL, [String: Any]) throws -> AVAudioRecorder) or protocol, using it can improve readability in call sites and docs.


306-315: Prefer moving the recorded file instead of reading/writing the entire Data

Loading the whole file into memory and writing it out can be wasteful for longer recordings. Moving the file avoids unnecessary allocations and is faster. Suggest:

-            let data = try Data(contentsOf: recorder.url.standardizedFileURL)
-            try data.write(to: newLocation)
+            try FileManager.default.moveItem(
+                at: recorder.url.standardizedFileURL,
+                to: newLocation
+            )

57-64: Fix small documentation typos and terminology

Minor nits in doc comments. Suggested edits:

  • Use “AudioRecorder” instead of “AudioPlayer” in Configuration docs.
  • “bused” -> “used”
  • “observer the duration” -> “observe the duration”
  • “customise of the underline ...” -> “customise the underlying audio session configuration ...”
  • “ensures that there will be conflicts ...” -> “ensures that there will be no conflicts ...”
  • Grammar fixes in background/foreground handlers.

Apply these diffs:

-        /// The interval at which the `AudioPlayer` will be fetching updates on the active `AVAudioPlayer`
+        /// The interval at which the `AudioRecorder` will be fetching updates on the active `AVAudioRecorder`
         /// meters.
-        /// The interval at which the `AudioPlayer` will be fetching updates on the duration of the
+        /// The interval at which the `AudioRecorder` will be fetching updates on the duration of the
         /// recorded track.
-        /// The default Configuration that is being bused by `StreamAudioRecorder`
+        /// The default configuration that is being used by `StreamAudioRecorder`
-    /// to observer the duration of the recorded track.
+    /// to observe the duration of the recorded track.
-    /// Provides a way to customise of the underline audioSessionConfiguration, in order to allow extension
-    /// on the logic that handles the `AVAudioSession.shared`.
+    /// Provides a way to customise the underlying audio session configuration, in order to allow extension
+    /// of the logic that handles `AVAudioSession.shared`.
-            .appendingPathComponent(UUID().uuidString) // Using UUID here ensures that there will be conflicts between file names
+            .appendingPathComponent(UUID().uuidString) // Using UUID here ensures there will be no conflicts between file names
             .appendingPathExtension(configuration.audioRecorderFileExtension) // Use the file extension provided with configuration
-        /// If an we move to the background then we want to stop the recording as we don't
+        /// If we move to the background, we want to stop the recording as we don't
         /// have the ability to pause and resume it afterwards.
-        /// Once we return to the foreground and the execution return back to us, we have an opportunity
+        /// Once we return to the foreground and the execution returns to us, we have an opportunity
         /// to resume the interrupted recording.

Also applies to: 81-82, 135-137, 195-200, 304-306, 355-358, 361-365


144-147: Add a small unit test to prove external subclassing works

Given IOS-1076’s goal, consider a test that verifies an external subclass can call super.init(configuration:audioSessionConfigurator:) and that the required init() remains available. I can draft this if helpful.

Here’s a quick outline for a test subclass within the module tests (or a fixture in a test helper module):

final class TestAudioRecorderSubclass: StreamAudioRecorder {
    var didInit = false

    public override init(
        configuration: Configuration,
        audioSessionConfigurator: AudioSessionConfiguring = StreamAudioSessionConfigurator()
    ) {
        super.init(configuration: configuration, audioSessionConfigurator: audioSessionConfigurator)
        didInit = true
    }
}

func test_subclass_can_call_designated_super_init() {
    let sut = TestAudioRecorderSubclass(configuration: .default)
    XCTAssertTrue(sut.didInit)
}

If you want, I can open a follow-up PR to add this.

Also applies to: 148-167


148-167: Optionally expose more DI via additional public defaults (only if needed)

If customers also need to override the meter normaliser or AVAudioRecorder creation, consider overloads with additional parameters defaulted to current values. Keep as-is unless such requests arise; your current API is appropriately conservative.


302-312: Minor: ensure you don’t leave the temp file behind (only if you keep copying)

If you keep the current copy approach, consider deleting the original temp file after write succeeds to avoid orphaned files.


148-167: Optional: consolidate init duplication

Both initializers duplicate multicastDelegate = .init() and call setUp(). Not a big deal, but you could move common work to a small postInit() called after super.init() in both places to DRY. Given Swift’s initializer rules, current version is acceptable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • 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 6c75969 and 749e068.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
🧬 Code Graph Analysis (1)
Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (1)
Sources/StreamChat/Audio/AudioPlayer/AudioPlaying.swift (1)
  • setUp (226-283)
🔇 Additional comments (6)
Sources/StreamChat/Audio/AudioRecorder/AudioRecording.swift (6)

148-167: Designated public init resolves subclassing limitation — good fix

Making init(configuration:audioSessionConfigurator:) a designated initializer (with a sensible default for the configurator), then calling super.init() followed by setUp(), is correct and enables external subclasses to properly call super.init(...). This aligns with the goal to allow customers to override the recorder.


195-203: configure(_:) remains useful for runtime swaps

Keeping configure(_:) is good for runtime replacement of the session configurator without re-instantiation. No action needed.


210-226: Completion handler semantics match docs

Completion is invoked only upon successful start; errors go to the delegate. Behavior aligns with the docstring.


428-452: Observers lifecycle is correct

Clean stop before re-register, weak captures, and resume calls look good. This helps avoid duplicate timers and leaks.


486-489: Meter normalisation use is correct

Normalising averagePower(forChannel: 0) before publishing context is appropriate.


148-167: Double-check StreamAudioRecorder init usage

I ran ripgrep across the Swift sources and found no occurrences of:

  • Direct StreamAudioRecorder(…) calls
  • Subclasses of StreamAudioRecorder
  • super.init(configuration: …) invocations

However, absence of matches isn’t proof that no call sites or subclass initializers depend on the old convenience initializer. Please manually verify across the codebase (including tests and example apps) that no existing initializations or subclass init implementations rely on the previous convenience signature.

Copy link

Public Interface

 open class StreamAudioRecorder: NSObject, AudioRecording, AVAudioRecorderDelegate, AppStateObserverDelegate  
-   public convenience init(configuration: Configuration)
+   public init(configuration: Configuration,audioSessionConfigurator: AudioSessionConfiguring = StreamAudioSessionConfigurator())

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@martinmitrevski martinmitrevski merged commit 01052a9 into develop Aug 19, 2025
12 of 14 checks passed
@martinmitrevski martinmitrevski deleted the audio-recorder-init-methods branch August 19, 2025 09:04
@Stream-SDK-Bot Stream-SDK-Bot mentioned this pull request Aug 21, 2025
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.

4 participants