Skip to content

Commit 08e2701

Browse files
Initial commit
1 parent 6901eaf commit 08e2701

File tree

22 files changed

+344
-215
lines changed

22 files changed

+344
-215
lines changed

packages/client/src/helpers/DynascaleManager.ts

Lines changed: 136 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -611,8 +611,15 @@ export class DynascaleManager {
611611
};
612612
};
613613

614-
private getOrCreateAudioContext = (): AudioContext | undefined => {
615-
if (this.audioContext || !isSafari()) return this.audioContext;
614+
private getOrCreateAudioContext = ({
615+
ignoreSafariCheck = false,
616+
}: {
617+
ignoreSafariCheck?: boolean;
618+
} = {}): AudioContext | undefined => {
619+
if (!ignoreSafariCheck && !isSafari()) return undefined;
620+
621+
if (this.audioContext) return this.audioContext;
622+
616623
const context = new AudioContext();
617624
if (context.state === 'suspended') {
618625
document.addEventListener('click', this.resumeAudioContext);
@@ -627,13 +634,133 @@ export class DynascaleManager {
627634
};
628635

629636
private resumeAudioContext = () => {
630-
if (this.audioContext?.state === 'suspended') {
631-
this.audioContext
632-
.resume()
633-
.catch((err) => this.logger('warn', `Can't resume audio context`, err))
634-
.then(() => {
635-
document.removeEventListener('click', this.resumeAudioContext);
637+
if (this.audioContext?.state !== 'suspended') return;
638+
639+
this.audioContext
640+
.resume()
641+
.catch((error) =>
642+
this.logger('warn', `Can't resume audio context`, error),
643+
)
644+
.then(() => {
645+
document.removeEventListener('click', this.resumeAudioContext);
646+
});
647+
};
648+
649+
public connectAudio = () => {
650+
const ac = this.getOrCreateAudioContext({ ignoreSafariCheck: true });
651+
652+
if (!ac) return () => {};
653+
654+
const audioSourceNodeBySessionId = new Map<
655+
string,
656+
MediaStreamAudioSourceNode | undefined
657+
>();
658+
const scAudioSourceNodeBySessionId = new Map<
659+
string,
660+
MediaStreamAudioSourceNode | undefined
661+
>();
662+
663+
const updateSinkId = (
664+
deviceId: string,
665+
audioContext: AudioContext | undefined,
666+
) => {
667+
if (!deviceId) return;
668+
669+
if (audioContext && 'setSinkId' in audioContext) {
670+
// @ts-expect-error setSinkId is not available in all browsers
671+
audioContext.setSinkId(deviceId).catch((error) => {
672+
this.logger('warn', `Couldn't set sinkId on AudioContext`, error);
636673
});
637-
}
674+
}
675+
};
676+
677+
// const volumeSubscription = combineLatest([
678+
// this.speaker.state.volume$,
679+
// participant$.pipe(distinctUntilKeyChanged('audioVolume')),
680+
// ]).subscribe(([volume, p]) => {
681+
// const participantVolume = p.audioVolume ?? volume;
682+
// audioElement.volume = participantVolume;
683+
// if (gainNode) gainNode.gain.value = participantVolume;
684+
// });
685+
686+
const sinkIdSubscription = !('setSinkId' in audioElement)
687+
? null
688+
: this.speaker.state.selectedDevice$.subscribe((deviceId) => {
689+
const audioContext = this.getOrCreateAudioContext();
690+
updateSinkId(deviceId, audioContext);
691+
});
692+
693+
const subscription = this.callState.rawParticipants$.subscribe(
694+
(current) => {
695+
const updatedSessionIds = new Set<string>();
696+
697+
current.forEach((p) => {
698+
if (p.isLocalParticipant) return;
699+
700+
updatedSessionIds.add(p.sessionId);
701+
702+
const asn = audioSourceNodeBySessionId.get(p.sessionId);
703+
704+
if (
705+
typeof asn !== 'undefined' &&
706+
typeof p.audioStream !== 'undefined' &&
707+
p.audioStream !== asn.mediaStream
708+
) {
709+
asn.disconnect();
710+
audioSourceNodeBySessionId.delete(p.sessionId);
711+
}
712+
713+
const scasn = scAudioSourceNodeBySessionId.get(p.sessionId);
714+
715+
if (
716+
typeof scasn !== 'undefined' &&
717+
typeof p.audioStream !== 'undefined' &&
718+
p.audioStream !== scasn.mediaStream
719+
) {
720+
scasn.disconnect();
721+
audioSourceNodeBySessionId.delete(p.sessionId);
722+
}
723+
724+
if (p.audioStream && !audioSourceNodeBySessionId.has(p.sessionId)) {
725+
const source = ac.createMediaStreamSource(p.audioStream);
726+
727+
source.connect(ac.destination);
728+
729+
audioSourceNodeBySessionId.set(p.sessionId, source);
730+
}
731+
732+
if (
733+
p.screenShareAudioStream &&
734+
!scAudioSourceNodeBySessionId.has(p.sessionId)
735+
) {
736+
const source = ac.createMediaStreamSource(p.screenShareAudioStream);
737+
738+
source.connect(ac.destination);
739+
740+
audioSourceNodeBySessionId.set(p.sessionId, source);
741+
}
742+
});
743+
744+
// cleanup removed participants
745+
746+
audioSourceNodeBySessionId.forEach((source, sessionId) => {
747+
if (updatedSessionIds.has(sessionId)) return;
748+
749+
source?.disconnect();
750+
audioSourceNodeBySessionId.delete(sessionId);
751+
});
752+
753+
scAudioSourceNodeBySessionId.forEach((source, sessionId) => {
754+
if (updatedSessionIds.has(sessionId)) return;
755+
756+
source?.disconnect();
757+
audioSourceNodeBySessionId.delete(sessionId);
758+
});
759+
760+
return () => {
761+
subscription.unsubscribe();
762+
};
763+
},
764+
);
638765
};
639766
}

packages/react-sdk/src/core/components/Audio/Audio.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ export const Audio = ({
3131

3232
useEffect(() => {
3333
if (!call || !audioElement) return;
34+
3435
const cleanup = call.bindAudioElement(audioElement, sessionId, trackType);
35-
return () => {
36-
cleanup?.();
37-
};
36+
37+
return cleanup;
3838
}, [call, sessionId, audioElement, trackType]);
3939

4040
return (

sample-apps/react/egress-composite/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
"@emotion/css": "^11.13.5",
1515
"@sentry/react": "^10.19.0",
1616
"@stream-io/video-react-sdk": "workspace:^",
17-
"clsx": "^2.0.0",
18-
"js-base64": "^3.7.8",
1917
"react": "19.1.0",
2018
"react-dom": "19.1.0"
2119
},
Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
:root {
2-
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
3-
font-size: 16px;
4-
line-height: 24px;
5-
font-weight: 400;
1+
@layer base-layer {
2+
:root {
3+
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
4+
font-size: 16px;
5+
line-height: 24px;
6+
font-weight: 400;
67

7-
font-synthesis: none;
8-
text-rendering: optimizeLegibility;
9-
-webkit-font-smoothing: antialiased;
10-
-moz-osx-font-smoothing: grayscale;
11-
-webkit-text-size-adjust: 100%;
12-
}
8+
font-synthesis: none;
9+
text-rendering: optimizeLegibility;
10+
-webkit-font-smoothing: antialiased;
11+
-moz-osx-font-smoothing: grayscale;
12+
-webkit-text-size-adjust: 100%;
13+
}
1314

14-
.str-video {
15-
color: var(--str-video__text-color1);
16-
position: relative;
17-
height: 100vh;
18-
}
15+
.str-video {
16+
color: var(--str-video__text-color1);
17+
position: relative;
18+
height: 100vh;
19+
}
1920

20-
body {
21-
margin: 0;
22-
min-width: 320px;
23-
min-height: 100vh;
24-
background-color: #000000;
25-
overflow: hidden;
26-
}
21+
body {
22+
margin: 0;
23+
min-width: 320px;
24+
min-height: 100vh;
25+
background-color: #000000;
26+
overflow: hidden;
27+
}
2728

28-
#root {
29-
margin: 0 auto;
30-
text-align: center;
29+
#root {
30+
margin: 0 auto;
31+
text-align: center;
32+
}
3133
}

sample-apps/react/egress-composite/src/CompositeApp.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
StreamTheme,
55
StreamVideo,
66
} from '@stream-io/video-react-sdk';
7-
import clsx from 'clsx';
7+
import { cx } from '@emotion/css';
88

99
import {
1010
EgressReadyNotificationProvider,
@@ -24,9 +24,7 @@ import { WithCustomActions } from './components/CustomActionsContext';
2424
export const CompositeApp = () => {
2525
const { client, call } = useInitializeClientAndCall();
2626

27-
// @ts-expect-error makes it easy to debug in the browser console
2827
window.call = call;
29-
// @ts-expect-error makes it easy to debug in the browser console
3028
window.client = client;
3129

3230
return (
@@ -57,7 +55,7 @@ export const StreamThemeWrapper = ({ children }: PropsWithChildren) => {
5755

5856
return (
5957
<StreamTheme
60-
className={clsx(
58+
className={cx(
6159
videoStyles,
6260
genericLayoutStyles,
6361
participantStyles,

sample-apps/react/egress-composite/src/ConfigurationContext.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
useMemo,
88
useState,
99
} from 'react';
10-
import { decode } from 'js-base64';
1110
import {
1211
LogLevel,
1312
ParticipantFilter,
@@ -179,7 +178,7 @@ export const extractPayloadFromToken = (
179178
if (!payload) throw new Error('Malformed token, missing payload');
180179

181180
try {
182-
return JSON.parse(decode(payload)) ?? {};
181+
return JSON.parse(atob(payload)) ?? {};
183182
} catch (e) {
184183
console.log('Error parsing token payload', e);
185184
return {};
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
.eca__logo-and-title-overlay {
1+
@layer base-layer {
2+
.eca__logo-and-title-overlay {
23
position: absolute;
34
display: grid;
45
top: 0;
56
right: 0;
6-
7+
78
width: 100%;
89
height: 100%;
9-
10+
1011
&__logo {
1112
position: absolute;
1213
}
1314
&__title {
1415
position: absolute;
1516
}
1617
}
17-
18+
}
Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
.eca__dominant-speaker__container {
2-
width: 100%;
3-
height: 100%;
4-
display: flex;
5-
justify-content: center;
6-
align-items: center;
1+
@layer base-layer {
2+
.eca__dominant-speaker__container {
3+
width: 100%;
4+
height: 100%;
5+
display: flex;
6+
justify-content: center;
7+
align-items: center;
78

8-
.str-video__participant-view {
9-
max-width: unset;
9+
.str-video__participant-view {
10+
max-width: unset;
11+
}
1012
}
1113
}
Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
.eca__dominant-speaker-screen-share__container {
2-
height: 100%;
3-
width: 100%;
4-
display: flex;
5-
flex-direction: column;
6-
align-items: center;
7-
justify-content: center;
1+
@layer base-layer {
2+
.eca__dominant-speaker-screen-share__container {
3+
height: 100%;
4+
width: 100%;
5+
display: flex;
6+
flex-direction: column;
7+
align-items: center;
8+
justify-content: center;
89

9-
.eca__dominant-speaker-screen-share__current-speaker {
10-
position: absolute;
11-
width: 240px;
12-
right: 10px;
13-
top: 10px;
14-
}
10+
.eca__dominant-speaker-screen-share__current-speaker {
11+
position: absolute;
12+
width: 240px;
13+
right: 10px;
14+
top: 10px;
15+
}
1516

16-
.str-video__participant-view {
17-
max-width: unset;
17+
.str-video__participant-view {
18+
max-width: unset;
19+
}
1820
}
1921
}

0 commit comments

Comments
 (0)