Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mobile-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@

- [Video Player](./video-player/) - minimal livestreaming viewer
- joining an existing livestream as a viewer

- [VoIP Call](./voip-call/) - two-user calling app with native call UI
- ringing through CallKit on iOS and Telecom on Android
- receiving calls while the app is backgrounded or killed
- APNs / FCM push signaling via a small Deno server
2 changes: 1 addition & 1 deletion mobile-react-native/blur-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ A mobile video chat demo showcasing **real-time camera background blur** using t

## License

This example is provided under the Apache License 2.0. See [LICENSE](../../../LICENSE) for details.
This example is provided under the Apache License 2.0.

---

Expand Down
2 changes: 1 addition & 1 deletion mobile-react-native/minimal-react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ A fully functional video room demo built with [Fishjam Cloud](https://fishjam.io

## License

This example is provided under the Apache License 2.0. See [LICENSE](../../../LICENSE) for details.
This example is provided under the Apache License 2.0.

---

Expand Down
2 changes: 1 addition & 1 deletion mobile-react-native/text-chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ yarn lint

## License

This example is provided under the Apache License 2.0. See [LICENSE](../../../LICENSE) for details.
This example is provided under the Apache License 2.0.

---

Expand Down
63 changes: 63 additions & 0 deletions mobile-react-native/voip-call/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# VoIP call example

A two-user calling app: an Expo React Native client (`app/`) and a small Deno
push + signaling server (`server/`). Calls ring through the native call UI
(CallKit on iOS, Telecom on Android) even when the app is backgrounded or
killed, and connect through a Fishjam room.

How it all works (native configuration, `VoIPProvider`, and the JS call flow)
is documented in the
[VoIP calls guide](https://documentation.fishjam.io/docs/how-to/client/voip-calls);
this README only covers running the example.

## Running the example

VoIP pushes can only be delivered through your own Apple and Firebase accounts,
so the first step is minting push credentials. Configure APNs to call iOS
devices, FCM to call Android devices, or both to call between them.

1. **Server credentials.** Create the APNs VoIP certificate (`apns.pem`) and/or
the FCM service account key (`fcm-credentials.json`) as described in
[`server/README.md`](./server/README.md), then start the server:

```bash
cd server
deno task start # listens on :4400
```

2. **`google-services.json` (Android only).** The file is gitignored, so you
have to fetch your own. In the
[Firebase console](https://console.firebase.google.com/), open the same
project the server's `fcm-credentials.json` came from → _Project settings_ →
_Your apps_ → add an **Android** app if none exists → download
`google-services.json` → save it at `app/google-services.json`.

Its `package_name` must match `android.package` in `app.json` exactly
(`io.fishjam.example.voipcall`), or the build fails with
`No matching client found for package name`. Keep the file at the app root:
`expo prebuild` copies it into `android/` for you, and a copy placed inside
`android/` by hand doesn't survive `expo prebuild --clean`.

3. **App environment.** Copy `app/.env.example` to `app/.env` and fill it in:

- `EXPO_PUBLIC_FISHJAM_ID`: your Fishjam app id.
- `EXPO_PUBLIC_SANDBOX_API_URL`: the Sandbox API url from your Fishjam
dashboard, used to mint peer tokens.
- `EXPO_PUBLIC_VOIP_SERVER_URL`: where the devices can reach the server
above. Not `localhost` when running on a physical phone; use your
machine's LAN address.

4. **Run the app.** iOS needs a real device — CallKit's incoming call screen
doesn't work on the Simulator. Android runs on an emulator with Google Play
services. Install dependencies, then:

```bash
cd app
yarn
yarn ios # or: yarn android
```

To test it, register two users on two devices and call between them. The
[guide's checklist](https://documentation.fishjam.io/docs/how-to/client/voip-calls#11-test-it)
lists the paths worth checking: ringing with the app killed, Recents redial,
and Hold & Accept.
3 changes: 3 additions & 0 deletions mobile-react-native/voip-call/app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
EXPO_PUBLIC_FISHJAM_ID=
EXPO_PUBLIC_SANDBOX_API_URL=
EXPO_PUBLIC_VOIP_SERVER_URL=http://localhost:4400
16 changes: 16 additions & 0 deletions mobile-react-native/voip-call/app/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
root: true,
extends: ['expo'],
ignorePatterns: [
'dist/*',
'node_modules/*',
'ios/*',
'android/*',
'server/*',
'.eslintrc.js',
'prettier.config.js',
],
rules: {
'import/no-unresolved': 'off',
},
};
24 changes: 24 additions & 0 deletions mobile-react-native/voip-call/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# dependencies
node_modules/

# Expo
.expo/
dist/
expo-env.d.ts

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android

google-services.json
107 changes: 107 additions & 0 deletions mobile-react-native/voip-call/app/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
FishjamProvider,
useVoIP,
VoIPProvider,
} from '@fishjam-cloud/react-native-client';
import { StatusBar } from 'expo-status-bar';
import { useCallback, useEffect, useRef } from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import type { VoIPIncomingPayload } from '@fishjam-cloud/react-native-client';
import {
useCallSignaling,
type SendSignalRef,
} from './src/hooks/useCallSignaling';
import { useDeviceRegistration } from './src/hooks/useDeviceRegistration';
import { useRecentsRedial } from './src/hooks/useRecentsRedial';
import { useRequestPermissions } from './src/hooks/useRequestPermissions';
import { CallScreen } from './src/screens/CallScreen';
import { LoginScreen } from './src/screens/LoginScreen';
import { UsersScreen } from './src/screens/UsersScreen';
import { BrandColors } from './src/theme/colors';
import { useUser } from './src/user/UserContext';
import { UserProvider } from './src/user/UserProvider';

function Main({ sendSignalRef }: { sendSignalRef: SendSignalRef }) {
const { username, isLoading } = useUser();
const { callStatus, lastEndedReason } = useVoIP();

useRequestPermissions();
useCallSignaling(sendSignalRef);
useDeviceRegistration();
useRecentsRedial();

useEffect(() => {
if (!lastEndedReason) return;
console.log(
`On user: ${username}, [VoIP] Call ended — reason: ${lastEndedReason}`,
);
}, [lastEndedReason, username]);

// Checked before the session gates below: a call can exist while the user session
// is still loading (answering a VoIP push right after a cold start) or missing
// (still registered for pushes after a logout), and it must connect regardless.
if (callStatus === 'connecting' || callStatus === 'active') {
return <CallScreen />;
}

if (isLoading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color={BrandColors.darkBlue80} />
</View>
);
}

if (!username) {
return <LoginScreen />;
}

return <UsersScreen />;
}

function VoIPApp() {
const sendSignalRef: SendSignalRef = useRef(undefined);

// A call we declined while another one was ringing never reaches the callee's
// signaling flow, so tell the caller ourselves.
const onWaitingCallDeclined = useCallback((payload: VoIPIncomingPayload) => {
sendSignalRef.current?.({
type: 'call-rejected',
to: payload.handle,
roomName: payload.roomName,
});
}, []);

return (
<FishjamProvider fishjamId={process.env.EXPO_PUBLIC_FISHJAM_ID ?? ''}>
<VoIPProvider onWaitingCallDeclined={onWaitingCallDeclined} isVideo>
<View style={styles.root}>
<StatusBar style="dark" />
<Main sendSignalRef={sendSignalRef} />
</View>
</VoIPProvider>
</FishjamProvider>
);
}

const App = () => (
<SafeAreaProvider>
<UserProvider>
<VoIPApp />
</UserProvider>
</SafeAreaProvider>
);

export default App;

const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: BrandColors.seaBlue20 },
center: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: BrandColors.seaBlue20,
},
});
26 changes: 26 additions & 0 deletions mobile-react-native/voip-call/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# voip-call-app

The Expo React Native client of the VoIP call example. Setup and run
instructions (push credentials, `google-services.json`, `.env`) are in the
[example README](../README.md) one level up; how the integration works is in
the [VoIP calls guide](https://documentation.fishjam.io/docs/how-to/client/voip-calls).

## Run

With the [server](../server/README.md) running and `.env` filled in:

```bash
yarn ios # or: yarn android
```

iOS needs a real device: CallKit's incoming call screen doesn't work on the
Simulator. Android runs on an emulator with Google Play services.

## iOS native registration

On Expo, the app depends on
[`@fishjam-cloud/ios-expo-voip`](https://www.npmjs.com/package/@fishjam-cloud/ios-expo-voip),
which registers the native subscriptions the SDK needs at launch: PushKit and
the Recents redial intent forwarding. Without the PushKit registration the app
never receives `didReceiveIncomingPush`, so VoIP pushes don't work at all, and
tap-to-redial silently does nothing.
69 changes: 69 additions & 0 deletions mobile-react-native/voip-call/app/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"expo": {
"name": "voip-call",
"slug": "voip-call",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "voipcall",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"bundleIdentifier": "io.fishjam.example.voipcall",
"infoPlist": {
"NSMicrophoneUsageDescription": "Allow $(PRODUCT_NAME) to access your microphone.",
"NSCameraUsageDescription": "Allow $(PRODUCT_NAME) to access your camera for video calls.",
"ITSAppUsesNonExemptEncryption": false
},
"entitlements": {
"aps-environment": "development"
},
"appleTeamId": "J5FM626PE2"
},
"android": {
"package": "io.fishjam.example.voipcall",
"googleServicesFile": "./google-services.json",
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"monochromeImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#F1FAFE"
},
"edgeToEdgeEnabled": true,
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_WIFI_STATE"
]
},
"web": {
"favicon": "./assets/images/favicon.png"
},
"plugins": [
[
"@fishjam-cloud/react-native-client",
{
"android": {
"enableVoIP": true
},
"ios": {
"enableVoIPBackgroundMode": true
},
"voip": {
"incomingCallTimeout": 20,
"outgoingCallTimeout": 20
}
}
],
[
"expo-splash-screen",
{
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#F1FAFE"
}
]
]
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions mobile-react-native/voip-call/app/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};
6 changes: 6 additions & 0 deletions mobile-react-native/voip-call/app/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerRootComponent } from 'expo';
import 'react-native-get-random-values';

import App from './App';

registerRootComponent(App);
33 changes: 33 additions & 0 deletions mobile-react-native/voip-call/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "voip-call",
"main": "index.js",
"version": "0.29.0",
"private": true,
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"lint": "eslint ."
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@fishjam-cloud/ios-expo-voip": "0.29.0",
"@fishjam-cloud/react-native-client": "0.30.0-rc.1",
"@react-native-async-storage/async-storage": "^3.1.1",
"expo": "~54.0.30",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-get-random-values": "1.11.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
"eslint": "^8.57.1",
"eslint-config-expo": "~8.0.1",
"typescript": "~5.9.2"
}
}
Loading