diff --git a/dailyjs/shared/components/Audio/Audio.js b/dailyjs/shared/components/Audio/Audio.js
index a0e1497..07271c5 100644
--- a/dailyjs/shared/components/Audio/Audio.js
+++ b/dailyjs/shared/components/Audio/Audio.js
@@ -1,79 +1,70 @@
/**
* Audio
- * ---
- * Renders audio tags for each audible participant / screen share in the call
- * Note: it's very important to minimise DOM mutates for audio components
- * as iOS / Safari do a lot of browser 'magic' that may result in muted
- * tracks. We heavily memoize this component to avoid unnecassary re-renders.
*/
-import React, { useRef, useEffect } from 'react';
-import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
-import useAudioTrack from '@dailyjs/shared/hooks/useAudioTrack';
-import PropTypes from 'prop-types';
+import React, { useEffect, useMemo } from 'react';
+import { useTracks } from '@dailyjs/shared/contexts/TracksProvider';
+import Bowser from 'bowser';
+import { Portal } from 'react-portal';
+import AudioTrack from './AudioTrack';
+import CombinedAudioTrack from './CombinedAudioTrack';
-const AudioItem = React.memo(
- ({ participant }) => {
- const audioRef = useRef(null);
- const audioTrack = useAudioTrack(participant);
+export const Audio = () => {
+ const { audioTracks } = useTracks();
- useEffect(() => {
- if (!audioTrack || !audioRef.current) return;
+ const renderedTracks = useMemo(
+ () =>
+ Object.entries(audioTracks).reduce(
+ (tracks, [id, track]) => ({ ...tracks, [id]: track }),
+ {}
+ ),
+ [audioTracks]
+ );
- // quick sanity to check to make sure this is an audio track...
- if (audioTrack.kind !== 'audio') return;
+ // On iOS safari, when headphones are disconnected, all audio elements are paused.
+ // This means that when a user disconnects their headphones, that user will not
+ // be able to hear any other users until they mute/unmute their mics.
+ // To fix that, we call `play` on each audio track on all devicechange events.
+ useEffect(() => {
+ const playTracks = () => {
+ document.querySelectorAll('.audioTracks audio').forEach(async (audio) => {
+ try {
+ if (audio.paused && audio.readyState === audio.HAVE_ENOUGH_DATA) {
+ await audio?.play();
+ }
+ } catch (e) {
+ // Auto play failed
+ }
+ });
+ };
+ navigator.mediaDevices.addEventListener('devicechange', playTracks);
+ return () => {
+ navigator.mediaDevices.removeEventListener('devicechange', playTracks);
+ };
+ }, []);
- audioRef.current.srcObject = new MediaStream([audioTrack]);
- }, [audioTrack]);
-
- useEffect(() => {
- // On iOS safari, when headphones are disconnected, all audio elements are paused.
- // This means that when a user disconnects their headphones, that user will not
- // be able to hear any other users until they mute/unmute their mics.
- // To fix that, we call `play` on each audio track on all devicechange events.
- if (audioRef.currenet) {
- return false;
- }
- const startPlayingTrack = () => {
- audioRef.current?.play();
- };
-
- navigator.mediaDevices.addEventListener(
- 'devicechange',
- startPlayingTrack
- );
-
- return () =>
- navigator.mediaDevices.removeEventListener(
- 'devicechange',
- startPlayingTrack
- );
- }, [audioRef]);
-
- return (
- <>
-
- >
- );
- },
- () => true
-);
-
-AudioItem.propTypes = {
- participant: PropTypes.object,
-};
-
-export const Audio = React.memo(() => {
- const { allParticipants } = useParticipants();
+ const tracksComponent = useMemo(() => {
+ const { browser } = Bowser.parse(navigator.userAgent);
+ if (browser.name === 'Chrome' && parseInt(browser.version, 10) >= 92) {
+ return ;
+ }
+ return Object.entries(renderedTracks).map(([id, track]) => (
+
+ ));
+ }, [renderedTracks]);
return (
- <>
- {allParticipants.map(
- (p) => !p.isLocal &&
- )}
- >
+
+
+ {tracksComponent}
+
+
+
);
-});
+};
export default Audio;
diff --git a/dailyjs/shared/components/Audio/AudioTrack.js b/dailyjs/shared/components/Audio/AudioTrack.js
new file mode 100644
index 0000000..e8e53ce
--- /dev/null
+++ b/dailyjs/shared/components/Audio/AudioTrack.js
@@ -0,0 +1,45 @@
+import React, { useRef, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const AudioTrack = React.memo(
+ ({ track }) => {
+ const audioRef = useRef(null);
+
+ useEffect(() => {
+ if (!audioRef.current) return false;
+ let playTimeout;
+
+ const handleCanPlay = () => {
+ playTimeout = setTimeout(() => {
+ console.log('Unable to autoplay audio element');
+ }, 1500);
+ };
+ const handlePlay = () => {
+ clearTimeout(playTimeout);
+ };
+ audioRef.current.addEventListener('canplay', handleCanPlay);
+ audioRef.current.addEventListener('play', handlePlay);
+ audioRef.current.srcObject = new MediaStream([track]);
+
+ const audioEl = audioRef.current;
+
+ return () => {
+ audioEl?.removeEventListener('canplay', handleCanPlay);
+ audioEl?.removeEventListener('play', handlePlay);
+ };
+ }, [track]);
+
+ return track ? (
+
+ ) : null;
+ },
+ () => true
+);
+
+AudioTrack.propTypes = {
+ track: PropTypes.object,
+};
+
+export default AudioTrack;
diff --git a/dailyjs/shared/components/Audio/CombinedAudioTrack.js b/dailyjs/shared/components/Audio/CombinedAudioTrack.js
new file mode 100644
index 0000000..87a1f26
--- /dev/null
+++ b/dailyjs/shared/components/Audio/CombinedAudioTrack.js
@@ -0,0 +1,60 @@
+import React, { useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useDeepCompareEffect, useDeepCompareMemo } from 'use-deep-compare';
+
+const CombinedAudioTrack = ({ tracks }) => {
+ const audioEl = useRef(null);
+
+ useEffect(() => {
+ if (!audioEl) return;
+ audioEl.current.srcObject = new MediaStream();
+ }, []);
+
+ const trackIds = useDeepCompareMemo(
+ () => Object.values(tracks).map((t) => t?.persistentTrack?.id),
+ [tracks]
+ );
+
+ useDeepCompareEffect(() => {
+ const audio = audioEl.current;
+ if (!audio || !audio.srcObject) return;
+
+ const stream = audio.srcObject;
+ const allTracks = Object.values(tracks);
+
+ allTracks.forEach((track) => {
+ const persistentTrack = track?.persistentTrack;
+ if (persistentTrack) {
+ persistentTrack.addEventListener(
+ 'ended',
+ (ev) => stream.removeTrack(ev.target),
+ { once: true }
+ );
+ stream.addTrack(persistentTrack);
+ }
+ });
+
+ audio.load();
+
+ if (
+ stream
+ .getAudioTracks()
+ .some((t) => t.enabled && t.readyState === 'live') &&
+ audio.paused
+ ) {
+ audio.play();
+ }
+ }, [tracks, trackIds]);
+
+ return (
+
+ );
+};
+
+CombinedAudioTrack.propTypes = {
+ tracks: PropTypes.object,
+};
+
+export default CombinedAudioTrack;
diff --git a/dailyjs/shared/package.json b/dailyjs/shared/package.json
index ed9c59a..975474a 100644
--- a/dailyjs/shared/package.json
+++ b/dailyjs/shared/package.json
@@ -13,6 +13,7 @@
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
+ "react-portal": "^4.2.1",
"shallow-equal": "^1.2.1",
"use-deep-compare": "^1.1.0"
}
diff --git a/yarn.lock b/yarn.lock
index 61dd260..b92f1b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2800,7 +2800,7 @@ progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-prop-types@15.7.2, prop-types@^15.7.2:
+prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -2907,6 +2907,13 @@ react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-portal@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6"
+ integrity sha512-fE9kOBagwmTXZ3YGRYb4gcMy+kSA+yLO0xnPankjRlfBv4uCpFXqKPfkpsGQQR15wkZ9EssnvTOl1yMzbkxhPQ==
+ dependencies:
+ prop-types "^15.5.8"
+
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"