Calls: Fix call sounds in iOS (#2216)

This commit is contained in:
Alexander Zinchuk 2023-02-22 23:48:39 +01:00
parent cbcfa354e6
commit 6d5625ce0c
6 changed files with 63 additions and 38 deletions

View File

@ -5,12 +5,13 @@ import { getActions, withGlobal } from './global';
import type { GlobalState } from './global/types';
import type { UiLoaderPage } from './components/common/UiLoader';
import { IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment';
import { IS_INSTALL_PROMPT_SUPPORTED, IS_MULTITAB_SUPPORTED, PLATFORM_ENV } from './util/environment';
import { INACTIVE_MARKER, PAGE_TITLE } from './config';
import { selectTabState } from './global/selectors';
import { updateSizes } from './util/windowSize';
import { addActiveTabChangeListener } from './util/activeTabMonitor';
import { hasStoredSession } from './util/sessions';
import { setupBeforeInstallPrompt } from './util/installPrompt';
import buildClassName from './util/buildClassName';
import { parseInitialLocationHash } from './util/routing';
import useFlag from './hooks/useFlag';
@ -55,6 +56,12 @@ const App: FC<StateProps> = ({
const { isMobile } = useAppLayout();
const isMobileOs = PLATFORM_ENV === 'iOS' || PLATFORM_ENV === 'Android';
useEffect(() => {
if (IS_INSTALL_PROMPT_SUPPORTED) {
setupBeforeInstallPrompt();
}
}, []);
// Prevent drop on elements that do not accept it
useEffect(() => {
const body = document.body;

View File

@ -1,4 +1,11 @@
import { IS_IOS, IS_SAFARI } from '../util/environment';
import { initializeSoundsForSafari } from '../global/actions/ui/calls';
export { default as GroupCall } from '../components/calls/group/GroupCall';
export { default as ActiveCallHeader } from '../components/calls/ActiveCallHeader';
export { default as PhoneCall } from '../components/calls/phone/PhoneCall';
export { default as RatePhoneCallModal } from '../components/calls/phone/RatePhoneCallModal';
if (IS_SAFARI || IS_IOS) {
document.addEventListener('click', initializeSoundsForSafari, { once: true });
}

View File

@ -32,6 +32,8 @@ import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink';
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
import { fastRaf } from '../../util/schedulers';
import { Bundles, loadBundle } from '../../util/moduleLoader';
import updateIcon from '../../util/updateIcon';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import useBackgroundMode from '../../hooks/useBackgroundMode';
@ -43,7 +45,7 @@ import useShowTransition from '../../hooks/useShowTransition';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useInterval from '../../hooks/useInterval';
import useAppLayout from '../../hooks/useAppLayout';
import updateIcon from '../../util/updateIcon';
import useTimeout from '../../hooks/useTimeout';
import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter';
@ -132,6 +134,7 @@ type StateProps = {
};
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
// eslint-disable-next-line @typescript-eslint/naming-convention
let DEBUG_isLogged = false;
@ -222,6 +225,11 @@ const Main: FC<OwnProps & StateProps> = ({
console.log('>>> RENDER MAIN');
}
// Preload Calls bundle to initialize sounds for iOS
useTimeout(() => {
void loadBundle(Bundles.Calls);
}, CALL_BUNDLE_LOADING_DELAY_MS);
const { isDesktop } = useAppLayout();
useEffect(() => {
if (!isLeftColumnOpen && !isMiddleColumnOpen && !isDesktop) {

View File

@ -6,7 +6,7 @@ import { updateChat } from '../../reducers';
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
import { notifyAboutCall } from '../../../util/notifications';
import { selectGroupCall, selectPhoneCallUser } from '../../selectors/calls';
import { checkNavigatorUserMediaPermissions, initializeSoundsForSafari } from '../ui/calls';
import { checkNavigatorUserMediaPermissions, initializeSounds } from '../ui/calls';
import { onTickEnd } from '../../../util/schedulers';
import type { ActionReturnType } from '../../types';
import { updateTabState } from '../../reducers/tabs';
@ -115,7 +115,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
});
});
void initializeSoundsForSafari();
initializeSounds();
void checkNavigatorUserMediaPermissions(global, actions, call.isVideo, getCurrentTabId());
global = {
...global,

View File

@ -26,16 +26,43 @@ import * as langProvider from '../../../util/langProvider';
import { updateTabState } from '../../reducers/tabs';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
// Workaround for Safari not playing audio without user interaction
// This is a tiny MP3 file that is silent - retrieved from https://bigsoundbank.com and then modified
// eslint-disable-next-line max-len
const silentSound = 'data:audio/mpeg;base64,SUQzBAAAAAABEVRYWFgAAAAtAAADY29tbWVudABCaWdTb3VuZEJhbmsuY29tIC8gTGFTb25vdGhlcXVlLm9yZwBURU5DAAAAHQAAA1N3aXRjaCBQbHVzIMKpIE5DSCBTb2Z0d2FyZQBUSVQyAAAABgAAAzIyMzUAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAAAAD/80DEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQsRbAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/zQMSkAAADSAAAAABVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV';
let audioElement: HTMLAudioElement | undefined;
let audioContext: AudioContext | undefined;
let sounds: Record<CallSound, HTMLAudioElement>;
let initializationPromise: Promise<void> | undefined = Promise.resolve();
export const initializeSoundsForSafari = () => {
if (!initializationPromise) return Promise.resolve();
// Workaround: this function is called once on the first user interaction.
// After that, it will be possible to play the notification on iOS without problems.
// https://rosswintle.uk/2019/01/skirting-the-ios-safari-audio-auto-play-policy-for-ui-sound-effects/
export function initializeSoundsForSafari() {
initializeSounds();
return Promise.all(Object.values(sounds).map((sound) => {
const prevSrc = sound.src;
sound.src = silentSound;
sound.muted = true;
sound.volume = 0.0001;
return sound.play()
.then(() => {
sound.pause();
sound.volume = 1;
sound.currentTime = 0;
sound.muted = false;
requestAnimationFrame(() => {
sound.src = prevSrc;
});
});
}));
}
export function initializeSounds() {
if (sounds) {
return;
}
const joinAudio = new Audio('./voicechat_join.mp3');
const connectingAudio = new Audio('./voicechat_connecting.mp3');
connectingAudio.loop = true;
@ -60,22 +87,7 @@ export const initializeSoundsForSafari = () => {
busy: busyAudio,
ringing: ringingAudio,
};
initializationPromise = Promise.all(Object.values(sounds).map((sound) => {
sound.muted = true;
sound.volume = 0.0001;
return sound.play().then(() => {
sound.pause();
sound.volume = 1;
sound.currentTime = 0;
sound.muted = false;
});
})).then(() => {
initializationPromise = undefined;
});
return initializationPromise;
};
}
async function fetchGroupCall<T extends GlobalState>(global: T, groupCall: Partial<ApiGroupCall>) {
const result = await callApi('getGroupCall', {
@ -258,7 +270,7 @@ addActionHandler('joinGroupCall', async (global, actions, payload): Promise<void
createAudioElement();
await initializeSoundsForSafari();
initializeSounds();
global = getGlobal();
void checkNavigatorUserMediaPermissions(global, actions, true, tabId);
@ -338,11 +350,7 @@ addActionHandler('playGroupCallSound', (global, actions, payload): ActionReturnT
safePlay(sounds[sound]);
};
if (initializationPromise) {
initializationPromise.then(doPlay);
} else {
doPlay();
}
doPlay();
});
addActionHandler('loadMoreGroupCallParticipants', (global): ActionReturnType => {
@ -376,7 +384,7 @@ addActionHandler('requestCall', async (global, actions, payload): Promise<void>
return;
}
await initializeSoundsForSafari();
initializeSounds();
global = getGlobal();
void checkNavigatorUserMediaPermissions(global, actions, isVideo, tabId);

View File

@ -8,8 +8,7 @@ import {
getActions, getGlobal,
} from './global';
import updateWebmanifest from './util/updateWebmanifest';
import { setupBeforeInstallPrompt } from './util/installPrompt';
import { IS_INSTALL_PROMPT_SUPPORTED, IS_MULTITAB_SUPPORTED } from './util/environment';
import { IS_MULTITAB_SUPPORTED } from './util/environment';
import './global/init';
import { APP_VERSION, DEBUG, MULTITAB_LOCALSTORAGE_KEY } from './config';
@ -59,10 +58,6 @@ async function init() {
updateWebmanifest();
if (IS_INSTALL_PROMPT_SUPPORTED) {
setupBeforeInstallPrompt();
}
TeactDOM.render(
<App />,
document.getElementById('root')!,