Skip to content

Add VoIP calling capabilties to fishjam - #78

Merged
Magmusacy merged 11 commits into
masterfrom
feature/voip-calls
Jul 29, 2026
Merged

Add VoIP calling capabilties to fishjam#78
Magmusacy merged 11 commits into
masterfrom
feature/voip-calls

Conversation

@Magmusacy

@Magmusacy Magmusacy commented Jul 28, 2026

Copy link
Copy Markdown

Description

Adds VoIP calling to the fork: native system call UI on both platforms, VoIP push delivery, and a single cross-platform JS API on top of them.

iOS — CallKit + PushKit

  • VoIP push registration and token delivery (getVoIPToken); an incoming call is reported to CallKit straight from the push payload, so the phone rings with the app killed.
  • Answer handshake: the native answer action is parked until JS reports media is live (fulfillIncomingCallConnected / failIncomingCallConnected), with a native timeout that tears the call down instead of leaving a stuck "Connecting…".
  • Outgoing calls with ringback (DialtonePlayer) and reportOutgoingCallConnected to start the system call timer.
  • Hold, mute, call waiting, and redial from Recents (getPendingCallIntentonCallIntent).

Android — Telecom + FCM

  • CallManager / TelecomController built on androidx.core:core-telecom: self-managed calls, system audio routing, and DisconnectCause mapping so calls land correctly in the call log.
  • PushNotificationService receives FCM data messages and relays anything that isn't a VoIP push to the app's own messaging service, so an existing FCM library keeps working alongside it.
  • Full-screen IncomingCallActivity over the lock screen, heads-up call notification with caller avatar (AvatarLoader), and a foreground service for the connected call.

Shared JS API

  • useVoIPEvents({ onIncoming, onAnswered, onEnded, onRegistered, onHeldChanged, onMuteChanged, onCallIntent, onWaitingCallDeclined }) — one hook, same events on both platforms, with pending call/answer buffering so a cold start triggered by a call still delivers them.
  • Platform-agnostic helpers in src/VoIP.ts: getVoIPToken, setCallHeld, setCallMuted, isCallHeld, reportOutgoingCallConnected.
  • Unified CallEndedReason: local | rejected | missed | remote | answeredElsewhere | failed.

Motivation and Context

A calling app has to ring when it isn't running, show the OS call UI, respect audio routing and interop with other calls, and appear in the call log. Only CallKit and Telecom can do that, and they have to be driven from the same native module that owns the WebRTC audio session — so the integration belongs here rather than in a separate library.

Documentation impact

  • Documentation update required
  • Documentation updated in another PR — usage guides and the runnable voip-call example ship with the web-client-sdk PR
  • No documentation update required

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Magmusacy and others added 11 commits July 20, 2026 11:34
* Hook up kotlin

* Kotling CallManager impl

* Minor fixes + Java bridge methods

* Make telecom manage audio endpoints while in call

* Cleanup coroutines when call ends

* Hook up CallManager with WebRTCModule

* Add CallNotificationManager

* Make CallManager work with Notification manager

* Add getter for display name

* Add broadcast intent receiver for hangup/decline

* Add a incoming call activity (WIP)

* Add some methods to test the behavior without firebase

* Add permissions + application tags to androidmanifest in webrtc fork (WIP)

* Create hooks for telecom

* Add pixel inspired full screen activity when someone is calling

* De-singleton CallNotificationManager

* Add dev logging

* Add ongoing CallState notification to FGS

* Add common notification id

* Make the hangup button work correctly

* Integrate with FCM

* Fetch FCM token on app start

* Refactor the listeners into their own classes

* Add android to useVoIPEvents

* Fix ObjectAlreadyConsumedException

* OPA

* Add react warm-up when receiving call when the app is closed

* Remove testing only functions

* Allow the call to happen on lock screen

* Further improvements to swtiching from audiomanager to telecom while in call

* Revert "OPA"

This reverts commit 0fd97f2.

* Allow launching the main activity from remote devices (headphones)

* Do not immediately clear pending requests (user can close the app during ringing)

* Remove tags

* Add vibrator

* Format

* Make errors less generic

* Do not call to get voipToken on every app launch

* Prevent race condition

* Bring back registration token on app launch (ios)

* Get FID only once at start up

* Fix only emit telecomActionPerformed on Android

* Fix formatting in EventEmitter.ts

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address PR review: volatile telecom audio fields, foreground service without React

Make AudioOutputManager's telecom-routing fields volatile, since they're written
from CallManager's coroutine and read from the RN bridge thread. Also let
ForegroundServiceController post the call notification using the application
context instead of only reactContext, so a call started from a push before
React attaches still gets its foreground service.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tidying up after cursor

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Hook up kotlin

* Kotling CallManager impl

* Minor fixes + Java bridge methods

* Make telecom manage audio endpoints while in call

* Cleanup coroutines when call ends

* Hook up CallManager with WebRTCModule

* Add CallNotificationManager

* Make CallManager work with Notification manager

* Add getter for display name

* Add broadcast intent receiver for hangup/decline

* Add a incoming call activity (WIP)

* Add some methods to test the behavior without firebase

* Add permissions + application tags to androidmanifest in webrtc fork (WIP)

* Create hooks for telecom

* Add pixel inspired full screen activity when someone is calling

* De-singleton CallNotificationManager

* Add dev logging

* Add ongoing CallState notification to FGS

* Add common notification id

* Make the hangup button work correctly

* Integrate with FCM

* Fetch FCM token on app start

* Refactor the listeners into their own classes

* Add android to useVoIPEvents

* Fix ObjectAlreadyConsumedException

* OPA

* Add react warm-up when receiving call when the app is closed

* Remove testing only functions

* Allow the call to happen on lock screen

* Further improvements to swtiching from audiomanager to telecom while in call

* Revert "OPA"

This reverts commit 0fd97f2.

* Allow launching the main activity from remote devices (headphones)

* Do not immediately clear pending requests (user can close the app during ringing)

* Remove tags

* Add vibrator

* Format

* Make errors less generic

* Do not call to get voipToken on every app launch

* Prevent race condition

* Bring back registration token on app launch (ios)

* Get FID only once at start up

* Add better android endCall events

* Add better iOS endCall events

* Fix only emit telecomActionPerformed on Android

* Fix only emit telecomActionPerformed on Android

* Add voip answer to connect handshake (iOS)

* Add voip answer to connect hadnshake (android)

* Fix formatting in EventEmitter.ts

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Remove unused endpointJob/availableJob/muteJob in CallManager

They were assigned but never read or cancelled — structured
concurrency already tears them down with the parent addCall scope,
so launching without storing a reference is equivalent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add configurable timeouts for Incoming/Outgoing/Fulfill actions

* Add connecting state before streaming media is connected

* refactor: drop redundant Platform.select in useTelecom

Telecom.ts already guards every function body with an Android check, so
useTelecom's own Platform.select + emptyResult was defense duplicated on
top of defense already in place. useCallKit keeps its split since CallKit
has no Android native module to fall back to.

* Add iOS Recents and call-intent redial support

* Fix Recents redial by handling deprecated call intents

INStartAudioCallIntent/INStartVideoCallIntent are deprecated in favour of
INStartCallIntent, but the Phone app's Recents redial still delivers them.
Matching only INStartCallIntent made handleContinueUserActivity return NO
for those, silently dropping the intent so tapping a Recents entry never
started a call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add call hold and resume support

* Fix held call notification duration resetting on Android

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review: volatile telecom audio fields, foreground service without React

Make AudioOutputManager's telecom-routing fields volatile, since they're written
from CallManager's coroutine and read from the RN bridge thread. Also let
ForegroundServiceController post the call notification using the application
context instead of only reactContext, so a call started from a push before
React attaches still gets its foreground service.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Android native implementaiton for incoming call while ongoig is active

* Add iOS native implementation for incoming call while ongoing is active

* Add ringback tone

* Add avatar/app icon capabilities

* Surface mute to JS

* Tidying up after cursor

* Format + Lint

* Fix minor issues from merge

* Fix

* Fix clang-format in CallKitManager.m

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Set callkit mute status from JS

* Fix pointed out issues

* Generate clang module map (#71)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Hook up kotlin

* Kotling CallManager impl

* Minor fixes + Java bridge methods

* Make telecom manage audio endpoints while in call

* Cleanup coroutines when call ends

* Hook up CallManager with WebRTCModule

* Add CallNotificationManager

* Make CallManager work with Notification manager

* Add getter for display name

* Add broadcast intent receiver for hangup/decline

* Add a incoming call activity (WIP)

* Add some methods to test the behavior without firebase

* Add permissions + application tags to androidmanifest in webrtc fork (WIP)

* Create hooks for telecom

* Add pixel inspired full screen activity when someone is calling

* De-singleton CallNotificationManager

* Add dev logging

* Add ongoing CallState notification to FGS

* Add common notification id

* Make the hangup button work correctly

* Integrate with FCM

* Fetch FCM token on app start

* Refactor the listeners into their own classes

* Add android to useVoIPEvents

* Fix ObjectAlreadyConsumedException

* OPA

* Add react warm-up when receiving call when the app is closed

* Remove testing only functions

* Allow the call to happen on lock screen

* Further improvements to swtiching from audiomanager to telecom while in call

* Revert "OPA"

This reverts commit 0fd97f2.

* Allow launching the main activity from remote devices (headphones)

* Do not immediately clear pending requests (user can close the app during ringing)

* Remove tags

* Add vibrator

* Format

* Make errors less generic

* Do not call to get voipToken on every app launch

* Prevent race condition

* Bring back registration token on app launch (ios)

* Get FID only once at start up

* Add better android endCall events

* Add better iOS endCall events

* Fix only emit telecomActionPerformed on Android

* Fix only emit telecomActionPerformed on Android

* Add voip answer to connect handshake (iOS)

* Add voip answer to connect hadnshake (android)

* Fix formatting in EventEmitter.ts

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Remove unused endpointJob/availableJob/muteJob in CallManager

They were assigned but never read or cancelled — structured
concurrency already tears them down with the parent addCall scope,
so launching without storing a reference is equivalent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add configurable timeouts for Incoming/Outgoing/Fulfill actions

* Add connecting state before streaming media is connected

* refactor: drop redundant Platform.select in useTelecom

Telecom.ts already guards every function body with an Android check, so
useTelecom's own Platform.select + emptyResult was defense duplicated on
top of defense already in place. useCallKit keeps its split since CallKit
has no Android native module to fall back to.

* Add iOS Recents and call-intent redial support

* Fix Recents redial by handling deprecated call intents

INStartAudioCallIntent/INStartVideoCallIntent are deprecated in favour of
INStartCallIntent, but the Phone app's Recents redial still delivers them.
Matching only INStartCallIntent made handleContinueUserActivity return NO
for those, silently dropping the intent so tapping a Recents entry never
started a call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add call hold and resume support

* Fix held call notification duration resetting on Android

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review: volatile telecom audio fields, foreground service without React

Make AudioOutputManager's telecom-routing fields volatile, since they're written
from CallManager's coroutine and read from the RN bridge thread. Also let
ForegroundServiceController post the call notification using the application
context instead of only reactContext, so a call started from a push before
React attaches still gets its foreground service.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Android native implementaiton for incoming call while ongoig is active

* Add iOS native implementation for incoming call while ongoing is active

* Add ringback tone

* Add avatar/app icon capabilities

* Surface mute to JS

* Tidying up after cursor

* Format + Lint

* Fix minor issues from merge

* Fix

* Fix clang-format in CallKitManager.m

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Set callkit mute status from JS

* Add fallback FCM message forwarding

* Fix pointed out issues

* Fix waiting call declining

* Add handle to callkit sessions

* Omit handle + make callInRecents optional

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…t disconnect reasons (#76)

On iOS the fulfill-answer watchdog only reported the failure to CallKit
and wiped native state - onCallEnded never fired, so the JS side kept
the call (room, camera, mic) alive indefinitely. Notify JS with
"failed" before cleanup, mirroring Android. Also fix
timedOutPerformingAction comparing the action UUID instead of the call
UUID, which made that handler a no-op.

On Android, restore ERROR causes (reverted to LOCAL in #75 because
CallControl.disconnect rejects them) so JS receives "failed" instead of
"local", and sanitize causes at the single Telecom disconnect call site
instead - transactional Telecom only accepts LOCAL, REMOTE, MISSED and
REJECTED.
…id) (#77)

* Fix cross-thread races in CallKitManager and CallManager

iOS: confine all CallKitManager state mutation to the main queue (where
CXProvider delegate callbacks, CXCallController completions and ring
timers already run) by funneling the RN-bridge and PushKit entry points
onto it; make properties atomic for the off-main sync getters; reject an
incoming call when a waiting slot is taken or the current call is still
ringing; clean up the ghost pending call when reportNewIncomingCall
fails.

Android: run the addCall finally-teardown under the CallManager monitor
so it cannot interleave with register(); always tear down on Disconnect
even when the telecom action returns an error; guard handleAnswered
against duplicate/stale answers and reset pendingAnswerRequestId on
register.

Claude-Session: https://claude.ai/code/session_01CxbgHoCG7oX3EYVJfkWGbs

* Handle the incoming-push payload on the main queue

The slot switch (pendingIncomingCall writes and callbacks) ran on the
PushKit queue after the main-thread report returned, so a main-thread
cleanup could be overwritten by the late write and resurrect a ghost
pending call. The whole report-and-record step now runs in one
main-queue block; only the push completion stays on the PushKit queue.

Claude-Session: https://claude.ai/code/session_01CxbgHoCG7oX3EYVJfkWGbs

* Make VoipManager state properties atomic

token is written on the PushKit registry queue and read by a
blocking-synchronous getter on the JS thread; pendingIncomingCall is
written on main and the module worker queue and read on the JS thread.
A nonatomic copy setter can release the old value in the window between
another thread's getter loading the ivar and retaining it, which is a
use-after-free. Same fix CallKitManager already received.

Claude-Session: https://claude.ai/code/session_01CxbgHoCG7oX3EYVJfkWGbs
@Magmusacy
Magmusacy marked this pull request as ready for review July 28, 2026 14:45
Copilot AI review requested due to automatic review settings July 28, 2026 14:45

Copilot AI 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.

Pull request overview

This PR adds end-to-end VoIP calling support to the Fishjam React Native WebRTC module by bridging native call UIs and VoIP push handling across iOS (CallKit + PushKit + Siri Recents intent) and Android (Core-Telecom + FCM), and exposing a unified JS API/events layer.

Changes:

  • Introduces a new cross-platform JS VoIP surface (VoIP.ts, Telecom.ts, hooks) for tokens, buffered incoming calls, answer fulfillment, hold/mute, and call intent handling.
  • Expands iOS CallKit/PushKit integration with buffered call intents, hold/mute controls, answer-fulfillment timeouts, ringback audio, and richer end reasons.
  • Adds Android Core-Telecom call lifecycle, call-style notifications + full-screen UI, VoIP FCM service/relay, and foreground-service integration.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/VoIP.ts New cross-platform VoIP JS API (token, pending calls/intents, answer fulfillment, hold/mute, outgoing connected).
src/useVoIPEvents.ts Expands VoIP event hook to support Android Telecom events, call intents, hold/mute, end reasons, and answer request IDs.
src/useTelecom.ts New Android-only Telecom hook and event hook wrapper.
src/useCallKit.ts Extends CallKit hook API to support end reasons + hold state; adjusts service config typing.
src/Telecom.ts New Android Telecom JS bridge (start/end/hold/answered/active/pending request id).
src/PushKit.ts Removes legacy iOS-only PushKit helper surface (replaced by VoIP.ts).
src/index.ts Re-exports new VoIP/Telecom APIs, hooks, and types from the package entrypoint.
src/EventEmitter.ts Registers the new telecomActionPerformed native event (Android-only).
src/CallKit.ts Extends iOS CallKit JS bridge with handle support, end reasons, answer fulfillment, hold/mute, and pending answer request ID.
ios/RCTWebRTC/WebRTCModule+PushKit.m Renames/updates PushKit module integration to VoIPManager, adds waiting-declined + call-intent events and new native getters/clearers.
ios/RCTWebRTC/WebRTCModule+CallKit.m Extends native CallKit module methods for handle, end reason, fulfill/fail answer, outgoing connected, hold/mute, and pending request ID.
ios/RCTWebRTC/WebRTCModule.m Updates exported event constant name for VoIP push event.
ios/RCTWebRTC/WebRTCModule.h Renames VoIP push event constant symbol (string remains "voipPushEvent").
ios/RCTWebRTC/VoIPManager.m New PushKit + Siri Recents intent manager with buffering and waiting-call behavior.
ios/RCTWebRTC/VoipManager.m Removes old PushKit manager implementation.
ios/RCTWebRTC/VoIPManager.h New header for VoIPManager including call intent and waiting-call declined support.
ios/RCTWebRTC/VoipManager.h Removes old VoipManager header.
ios/RCTWebRTC/FulfillRequestManager.m New request/timeout manager for deferring CallKit answer fulfillment until media is live.
ios/RCTWebRTC/FulfillRequestManager.h Header for fulfill-request manager and result enum.
ios/RCTWebRTC/DialtonePlayer.m Adds synthesized ringback playback for outgoing calls during CallKit audio session activation.
ios/RCTWebRTC/DialtonePlayer.h Dialtone player interface documentation and API.
ios/RCTWebRTC/CallKitManager.m Major CallKit lifecycle enhancements: waiting calls, timeouts, end reasons, fulfill/timeout answer, hold/mute, ringback, and cleanup logic.
ios/RCTWebRTC/CallKitManager.h Updates CallKit manager public API for handles, end reasons, waiting slots, hold/mute, and pending answer request ID.
FishjamReactNativeWebrtc.podspec Enables DEFINES_MODULE to support Swift module import by downstream pods.
android/src/main/java/com/oney/WebRTCModule/WebRTCModule.java Wires new TelecomController/VoIPController and exposes new ReactMethods for Telecom/VoIP.
android/src/main/java/com/oney/WebRTCModule/VoIPController.java Bridges VoIP token/incoming/waiting-declined events and pending-call buffering to JS.
android/src/main/java/com/oney/WebRTCModule/voip/VoIPPushRegistry.kt Process-wide registry buffering VoIP token + incoming/waiting call payloads (killed-state safe).
android/src/main/java/com/oney/WebRTCModule/voip/VoIPForegroundServiceController.kt Maps call state transitions to foreground-service requests.
android/src/main/java/com/oney/WebRTCModule/voip/VoIPForegroundRequest.java Immutable model for call-driven foreground-service requirements.
android/src/main/java/com/oney/WebRTCModule/voip/PushNotificationService.kt New FCM messaging service handling VoIP pushes + relaying non-VoIP traffic to a fallback service.
android/src/main/java/com/oney/WebRTCModule/voip/LockScreenController.kt Enables showing the host activity over the lockscreen after answering.
android/src/main/java/com/oney/WebRTCModule/voip/IncomingCallActivity.kt Full-screen incoming-call UI (programmatic layout) + swipe answer/decline + avatar updates.
android/src/main/java/com/oney/WebRTCModule/voip/FulfillRequestManager.kt Kotlin counterpart for answer-fulfillment timeout management.
android/src/main/java/com/oney/WebRTCModule/voip/EndCallNotificationReceiver.kt Handles notification actions (decline/hangup/answer waiting) without launching an activity.
android/src/main/java/com/oney/WebRTCModule/voip/DialtonePlayer.kt Outgoing ringback playback using ToneGenerator on voice-call stream.
android/src/main/java/com/oney/WebRTCModule/voip/CallNotificationManager.kt Builds incoming/ongoing/waiting CallStyle notifications and vibration behavior.
android/src/main/java/com/oney/WebRTCModule/voip/CallManager.kt Core-Telecom call registration, lifecycle handling, timeouts, endpoints, notifications, avatars, and JS event dispatch.
android/src/main/java/com/oney/WebRTCModule/voip/AvatarLoader.kt Downloads and circular-crops caller avatars off-main-thread for notifications/UI.
android/src/main/java/com/oney/WebRTCModule/TelecomController.java Bridges CallManager lifecycle events and commands to React Native + lockscreen behavior.
android/src/main/java/com/oney/WebRTCModule/foregroundService/WebRTCForegroundService.java Posts CallStyle ongoing notification via FGS when a VoIP call is active.
android/src/main/java/com/oney/WebRTCModule/foregroundService/ForegroundServiceController.java Merges VoIP foreground requirements with existing camera/mic/screen-share requirements.
android/src/main/java/com/oney/WebRTCModule/AudioOutputManager.java Adds Telecom-aware endpoint routing + selection confirmation when Telecom owns audio.
android/build.gradle Enables Kotlin Android plugin, sets Kotlin JVM target, and adds core-telecom + messaging dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/useVoIPEvents.ts
Comment thread android/build.gradle
@Magmusacy
Magmusacy merged commit 2f4f2b7 into master Jul 29, 2026
9 checks passed
@Magmusacy
Magmusacy deleted the feature/voip-calls branch July 29, 2026 13:42
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.

3 participants