Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: multiple rare ringing issues in react-native #1611

Merged
merged 17 commits into from
Dec 13, 2024
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
10 changes: 7 additions & 3 deletions packages/client/src/devices/BrowserPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ export class BrowserPermission {
const isGranted = this.state === 'granted';

if (!isGranted && throwOnNotAllowed) {
throw new DOMException(
throw new Error(
'Permission was not granted previously, and prompting again is not allowed',
'NotAllowedError',
);
}

Expand All @@ -91,7 +90,12 @@ export class BrowserPermission {
this.setState('granted');
return true;
} catch (e) {
if (e instanceof DOMException && e.name === 'NotAllowedError') {
if (
e &&
typeof e === 'object' &&
'name' in e &&
(e.name === 'NotAllowedError' || e.name === 'SecurityError')
) {
this.logger('info', 'Browser permission was not granted', {
permission: this.permission,
});
Expand Down
39 changes: 21 additions & 18 deletions packages/client/src/devices/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,17 @@ const getStream = async (constraints: MediaStreamConstraints) => {
return stream;
};

function isOverconstrainedError(error: unknown) {
return (
error &&
typeof error === 'object' &&
(('name' in error && error.name === 'OverconstrainedError') ||
('message' in error &&
typeof error.message === 'string' &&
error.message.startsWith('OverconstrainedError')))
);
}

/**
* Returns an audio media stream that fulfills the given constraints.
* If no constraints are provided, it uses the browser's default ones.
Expand All @@ -194,18 +205,14 @@ export const getAudioStream = async (
});
return await getStream(constraints);
} catch (error) {
if (
error instanceof DOMException &&
error.name === 'OverconstrainedError' &&
trackConstraints?.deviceId
) {
const { deviceId, ...relaxedContraints } = trackConstraints;
if (isOverconstrainedError(error) && trackConstraints?.deviceId) {
const { deviceId, ...relaxedConstraints } = trackConstraints;
getLogger(['devices'])(
'warn',
'Failed to get audio stream, will try again with relaxed contraints',
{ error, constraints, relaxedContraints },
'Failed to get audio stream, will try again with relaxed constraints',
{ error, constraints, relaxedConstraints },
);
return getAudioStream(relaxedContraints);
return getAudioStream(relaxedConstraints);
}

getLogger(['devices'])('error', 'Failed to get audio stream', {
Expand Down Expand Up @@ -240,18 +247,14 @@ export const getVideoStream = async (
});
return await getStream(constraints);
} catch (error) {
if (
error instanceof DOMException &&
error.name === 'OverconstrainedError' &&
trackConstraints?.deviceId
) {
const { deviceId, ...relaxedContraints } = trackConstraints;
if (isOverconstrainedError(error) && trackConstraints?.deviceId) {
const { deviceId, ...relaxedConstraints } = trackConstraints;
getLogger(['devices'])(
'warn',
'Failed to get video stream, will try again with relaxed contraints',
{ error, constraints, relaxedContraints },
'Failed to get video stream, will try again with relaxed constraints',
{ error, constraints, relaxedConstraints },
);
return getVideoStream(relaxedContraints);
return getVideoStream(relaxedConstraints);
}

getLogger(['devices'])('error', 'Failed to get video stream', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { StyleSheet, View } from 'react-native';
import { useTheme } from '../../../contexts';
import { AcceptCallButton } from './AcceptCallButton';
import { RejectCallButton } from './RejectCallButton';
import { ToggleVideoPreviewButton } from './ToggleVideoPreviewButton';

/**
* Props for the IncomingCallControls Component.
Expand Down Expand Up @@ -36,7 +35,6 @@ export const IncomingCallControls = ({
size={buttonSizes.md}
rejectReason="decline"
/>
<ToggleVideoPreviewButton />
<AcceptCallButton onPressHandler={onAcceptCallHandler} />
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
IncomingCallControlsProps,
} from '../CallControls';
import { useTheme } from '../../../contexts';
import { useApplyDefaultMediaStreamSettings } from '../../../hooks/useApplyDefaultMediaStreamSettings';

/**
* Props for the IncomingCall Component.
Expand Down Expand Up @@ -49,8 +48,6 @@ export const IncomingCall = ({
theme: { colors, incomingCall, typefaces },
} = useTheme();

useApplyDefaultMediaStreamSettings();

const landscapeContentStyles: ViewStyle = {
flexDirection: landscape ? 'row' : 'column',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
useStreamVideoClient,
} from '@stream-io/video-react-bindings';
import { BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { filter, distinctUntilChanged } from 'rxjs/operators';
import { processCallFromPush } from '../../utils/push/internal/utils';
import { StreamVideoClient } from '@stream-io/video-client';
import type { StreamVideoConfig } from '../../utils/StreamVideoRN/types';
Expand Down Expand Up @@ -89,7 +89,7 @@ const createCallSubscription = (
action: 'accept' | 'decline' | 'pressed' | 'backgroundDelivered'
) => {
return behaviourSubjectWithCallCid
.pipe(filter(cidIsNotUndefined))
.pipe(filter(cidIsNotUndefined), distinctUntilChanged())
.subscribe(async (callCId) => {
await processCallFromPush(client, callCId, action, pushConfig);
behaviourSubjectWithCallCid.next(undefined); // remove the current call id to avoid processing again
Expand Down
33 changes: 30 additions & 3 deletions packages/react-native-sdk/src/hooks/useAutoEnterPiPEffect.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
import { CallingState } from '@stream-io/video-client';
import { useCallStateHooks } from '@stream-io/video-react-bindings';
import { useEffect } from 'react';
import { NativeModules, Platform } from 'react-native';

export function useAutoEnterPiPEffect(
disablePictureInPicture: boolean | undefined
) {
const { useCallCallingState } = useCallStateHooks();

const callingState = useCallCallingState();

// if we need to enable, only enable in joined state
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}

NativeModules.StreamVideoReactNative.canAutoEnterPipMode(
!disablePictureInPicture
);
if (!disablePictureInPicture && callingState === CallingState.JOINED) {
NativeModules.StreamVideoReactNative.canAutoEnterPipMode(
!disablePictureInPicture
);
}
}, [callingState, disablePictureInPicture]);

// on unmount always disable PiP mode
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}

return () => {
NativeModules.StreamVideoReactNative.canAutoEnterPipMode(false);
};
}, [disablePictureInPicture]);

// if disable prop was sent, immediately disable PiP mode
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}

if (disablePictureInPicture) {
NativeModules.StreamVideoReactNative.canAutoEnterPipMode(false);
}
}, [disablePictureInPicture]);
}
11 changes: 7 additions & 4 deletions packages/react-native-sdk/src/providers/StreamCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,24 @@ const AppStateListener = () => {
NativeModules?.StreamVideoReactNative?.isInPiPMode().then(
(isInPiP: boolean | null | undefined) => {
if (!isInPiP) {
const currentState = appState.current;
if (currentState === 'active') {
if (AppState.currentState === 'active') {
// this is to handle the case that the app became active as soon as it went to background
// in this case, we dont want to disable the camera
// this happens on foreground push notifications
return;
}
call?.camera?.disable();
if (call?.camera?.state.status === 'enabled') {
call?.camera?.disable();
}
}
}
);
} else {
// shouldDisableIOSLocalVideoOnBackgroundRef is false, if local video is enabled on PiP
if (shouldDisableIOSLocalVideoOnBackgroundRef.current) {
call?.camera?.disable();
if (call?.camera?.state.status === 'enabled') {
call?.camera?.disable();
}
}
}
appState.current = nextAppState;
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-sdk/src/utils/push/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,10 @@ export const firebaseDataHandler = async (
data,
android: {
channelId,
importance: 4, // high importance
foregroundServiceTypes: getIncomingCallForegroundServiceTypes(),
asForegroundService,
ongoing: true,
sound: incomingCallChannel.sound,
vibrationPattern: incomingCallChannel.vibrationPattern,
pressAction: {
Expand Down Expand Up @@ -305,6 +307,7 @@ export const firebaseDataHandler = async (
sound: callChannel.sound,
vibrationPattern: callChannel.vibrationPattern,
channelId,
importance: 4, // high importance
pressAction: {
id: 'default',
launchActivity: 'default', // open the app when the notification is pressed
Expand Down
Loading
Loading