Skip to content

[Enhancement] Extend CallJoinIntercepting with will-join and did-join hooks#1176

Draft
ipavlidakis wants to merge 2 commits into
developfrom
iliaspavlidakis/ios-1799-extend-joincallinterceptor-with-before-pc-connect-and
Draft

[Enhancement] Extend CallJoinIntercepting with will-join and did-join hooks#1176
ipavlidakis wants to merge 2 commits into
developfrom
iliaspavlidakis/ios-1799-extend-joincallinterceptor-with-before-pc-connect-and

Conversation

@ipavlidakis

@ipavlidakis ipavlidakis commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Links

IOS-1799

🎯 Goal

CallJoinIntercepting previously exposed a single gate, callReadyToJoin(_:), which only let integrators run (and optionally veto) work at the very last step of a join. That isn't enough for use cases that need to bracket the entire connection window — for example muting remote media while the peer connection is still negotiating and restoring it once the call is live. This PR adds two best-effort lifecycle hooks around the existing gate so integrators get a clear before/after pair.

📝 Summary

  • Added callWillJoin(_:) and callDidJoin(_:) to CallJoinIntercepting, with default no-op implementations so only callReadyToJoin(_:) stays required and existing conformers remain source-compatible.
  • Exposed CallController.stagePublisher, a projection of the WebRTC coordinator's state-machine stage IDs, so the join flow can react to lifecycle transitions without touching WebRTC internals.
  • JoiningStage invokes callWillJoin(_:) when WebRTC enters .joining; JoinedStage invokes callDidJoin(_:) once the call is fully joined.
  • Updated DemoCallJoinInterceptor to mute remote audio in callWillJoin and restore it in callDidJoin, and defaulted the debug feature flag to .synchronised.

🛠 Implementation

The three hooks now fire at distinct points in the join lifecycle:

Hook When it fires Blocking?
callWillJoin(_:) WebRTC state machine enters .joining (peer connection still negotiating) No (best-effort)
callReadyToJoin(_:) Backend join applied, before the call becomes active Yes — can throw to veto/delay
callDidJoin(_:) Call has fully transitioned into the joined state No (best-effort)

callWillJoin is wired through CallController.stagePublisher: JoiningStage.observeCallStage(_:joinCallInterceptor:) subscribes to it, filters for .joining, and calls the hook from a task stored in the stage-scoped DisposableBag (so it's torn down on transition away). callDidJoin is invoked from JoinedStage.execute() after the call-settings and own-capabilities subscriptions are in place, recovering the interceptor from the join input carried in the state-machine context.

The two new methods were named callWillJoin/callDidJoin (rather than pre/post) to follow the Apple will/did delegate convention and to keep them visually distinct from the throwing callReadyToJoin gate. They ship with default implementations, so the addition is non-source-breaking and needs no deprecations.

🧪 Manual Testing Notes

  1. Enable the CallJoinInterceptor debug feature flag (defaults to .synchronised in debug builds).
  2. Start/join a ringing call from two devices.
  3. Confirm remote audio is muted while the call is connecting and is restored automatically once the call is fully joined.

Automated coverage added:

  • StreamCallStateMachineStageJoiningStage_Tests.test_execute_whenWebRTCStageBecomesJoining_invokesInterceptorCallIsPreparing
  • CallStateMachineStageJoinedStage_Tests.test_joiningTransition_joinInterceptorProvided_callDidJoinInvoked
  • MockCallController gained a controllable stagePublisher to drive stage transitions.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

Summary by CodeRabbit

  • New Features

    • Added call-join interception hooks for “pre-join” and “joined” moments, enabling custom behavior during the call join flow.
    • Development builds now enable call-join interception by default.
  • Bug Fixes

    • Improved remote audio handling during call setup by muting remote participants while joining and restoring audio once connected.
    • Ensured join-related callbacks fire at the correct points in the call lifecycle.

@ipavlidakis ipavlidakis self-assigned this Jun 26, 2026
@ipavlidakis ipavlidakis added the enhancement New feature or request label Jun 26, 2026
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds callWillJoin(_:) and callDidJoin(_:) async hooks to CallJoinIntercepting with default no-op implementations, wires them into Call+JoiningStage and Call+JoinedStage via a new stagePublisher on CallController, implements media mute/restore in DemoCallJoinInterceptor, switches the demo default to .synchronised, and adds corresponding tests and mock support.

Changes

Call join interceptor lifecycle hooks

Layer / File(s) Summary
Protocol hooks
Sources/StreamVideo/Utils/CallJoinIntercepting.swift
Adds callWillJoin(_:) and callDidJoin(_:) async protocol requirements with default no-op extension implementations.
CallController stage publisher
Sources/StreamVideo/Controllers/CallController.swift
Adds stagePublisher exposing WebRTC coordinator stage IDs as a Combine AnyPublisher.
Joining/Joined stage wiring
Sources/StreamVideo/CallStateMachine/Stages/Call+JoiningStage.swift, Sources/StreamVideo/CallStateMachine/Stages/Call+JoinedStage.swift
Joining stage subscribes to stagePublisher, invoking callWillJoin on transition to .joining; joined stage forwards context.input and invokes callDidJoin after settings/capabilities subscriptions complete.
Demo interceptor media mute/restore
DemoApp/Sources/Components/DemoCallJoinInterceptor/DemoCallJoinInterceptor.swift, DemoApp/Sources/Components/Debug/Items/FeatureFlags/Components/CallJoinInterceptor.swift
Implements callWillJoin/callDidJoin to disable/restore remote participants' audio tracks, adds documentation for readiness handshake teardown, and changes the debug default from .none to .synchronised.
Tests and mock stage publisher
StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoinedStageTests.swift, StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoiningStageTests.swift, StreamVideoTests/Mock/MockCallController.swift
Adds spy interceptors and tests verifying hook invocation on stage transitions, and adds a controllable stageSubject/stagePublisher to MockCallController.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant JoiningStage
  participant CallController
  participant Interceptor as CallJoinIntercepting
  participant JoinedStage

  JoiningStage->>CallController: subscribe stagePublisher
  CallController-->>JoiningStage: stage == .joining
  JoiningStage->>Interceptor: callWillJoin(call)
  Interceptor->>Interceptor: disable remote audio tracks
  JoinedStage->>JoinedStage: subscribeToCallSettingsUpdates / subscribeToOwnCapabilitiesChanges
  JoinedStage->>Interceptor: callDidJoin(call)
  Interceptor->>Interceptor: restore remote audio tracks
Loading

Possibly related PRs

Suggested reviewers: martinmitrevski

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 change: adding will-join and did-join hooks to CallJoinIntercepting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iliaspavlidakis/ios-1799-extend-joincallinterceptor-with-before-pc-connect-and

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.

@ipavlidakis
ipavlidakis force-pushed the iliaspavlidakis/ios-1799-extend-joincallinterceptor-with-before-pc-connect-and branch from f9e7e98 to dcedd62 Compare June 26, 2026 10:45
@ipavlidakis ipavlidakis changed the title [Enhancement]Extend callJoinInterceptor hook flow [Enhancement] Extend CallJoinIntercepting with will-join and did-join hooks Jun 26, 2026
@ipavlidakis
ipavlidakis marked this pull request as ready for review July 6, 2026 11:45
@ipavlidakis
ipavlidakis requested a review from a team as a code owner July 6, 2026 11:45
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Public Interface

+ extension CallJoinIntercepting  
+ 
+   public func callWillJoin(_ call: Call)async 
+   public func callDidJoin(_ call: Call)async



@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamVideo 10.56 MB 10.56 MB +1 KB 🟢
StreamVideoSwiftUI 2.47 MB 2.47 MB 0 KB 🟢
StreamVideoUIKit 2.6 MB 2.6 MB 0 KB 🟢
StreamWebRTC 11.87 MB 11.87 MB 0 KB 🟢

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@DemoApp/Sources/Components/DemoCallJoinInterceptor/DemoCallJoinInterceptor.swift`:
- Around line 156-178: `disableMediaTracksAndCallSettings(_:)` is capturing
`currentUser` too early, so the `$participants` sink can compare against a stale
nil session id and mute the local participant. Move the local-participant lookup
into the participant stream mapping, using the current
`call.state.localParticipant` (or `call.state.sessionId`) when filtering
participants, so the self participant is excluded based on the latest joined
state. Keep the rest of the mute/restore flow in `DemoCallJoinInterceptor`
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43aded53-765f-4d14-a7ed-164a4853f8f1

📥 Commits

Reviewing files that changed from the base of the PR and between cedcab9 and 5783fb6.

📒 Files selected for processing (9)
  • DemoApp/Sources/Components/Debug/Items/FeatureFlags/Components/CallJoinInterceptor.swift
  • DemoApp/Sources/Components/DemoCallJoinInterceptor/DemoCallJoinInterceptor.swift
  • Sources/StreamVideo/CallStateMachine/Stages/Call+JoinedStage.swift
  • Sources/StreamVideo/CallStateMachine/Stages/Call+JoiningStage.swift
  • Sources/StreamVideo/Controllers/CallController.swift
  • Sources/StreamVideo/Utils/CallJoinIntercepting.swift
  • StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoinedStageTests.swift
  • StreamVideoTests/CallStateMachine/CallStateMachine/Stages/CallStateMachine_JoiningStageTests.swift
  • StreamVideoTests/Mock/MockCallController.swift

@Stream-SDK-Bot

Copy link
Copy Markdown
Collaborator

StreamVideo XCSize

Object Diff (bytes)
ApplicationLifecycleVideoMuteAdapter.o +5240
Call+JoiningStage.o +2267
Call+JoinedStage.o +978
CallController.o +416
CallJoinIntercepting.o +52
WebRTCStateAdapter.o -50

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@ipavlidakis
ipavlidakis marked this pull request as draft July 22, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants