diff --git a/src/components/production-line/production-line.tsx b/src/components/production-line/production-line.tsx index 8ed942fb..6c6062ee 100644 --- a/src/components/production-line/production-line.tsx +++ b/src/components/production-line/production-line.tsx @@ -65,8 +65,10 @@ const ButtonWrapper = styled.span` export const ProductionLine: FC = () => { const { productionId: paramProductionId, lineId: paramLineId } = useParams(); - const [{ joinProductionOptions, dominantSpeaker }, dispatch] = - useGlobalState(); + const [ + { joinProductionOptions, dominantSpeaker, audioLevelAboveThreshold }, + dispatch, + ] = useGlobalState(); const [isInputMuted, setIsInputMuted] = useState(true); const [isOutputMuted, setIsOutputMuted] = useState(false); @@ -261,6 +263,7 @@ export const ProductionLine: FC = () => { sessionid={sessionId} participants={line.participants} dominantSpeaker={dominantSpeaker} + audioLevelAboveThreshold={audioLevelAboveThreshold} /> )} diff --git a/src/components/production-line/rtc-stat-interval.ts b/src/components/production-line/rtc-stat-interval.ts new file mode 100644 index 00000000..8fe19818 --- /dev/null +++ b/src/components/production-line/rtc-stat-interval.ts @@ -0,0 +1,79 @@ +import { Dispatch } from "react"; +import { TGlobalStateAction } from "../../global-state/global-state-actions.ts"; + +export const startRtcStatInterval = ({ + rtcPeerConnection, + dispatch, +}: { + rtcPeerConnection: RTCPeerConnection; + dispatch: Dispatch; +}) => { + let ongoingStatsPromise: null | Promise = null; + + const statsInterval = window.setInterval(() => { + // Do not request new stats if previously requested has not yet resolved + if (ongoingStatsPromise) return; + + const inboundRtpStats: unknown[] = []; + const mediaSourceStats: unknown[] = []; + + const audioLevelThreshold = 0.02; + + let isAudioLevelAboveThreshold = false; + + ongoingStatsPromise = rtcPeerConnection.getStats().then((stats) => { + ongoingStatsPromise = null; + + stats.forEach((stat) => { + if (stat.type === "inbound-rtp") { + inboundRtpStats.push(stat); + } + + if (stat.type === "media-source") { + mediaSourceStats.push(stat); + } + }); + + // Check if we have incoming audio above a threshold + if (inboundRtpStats.length) { + inboundRtpStats.forEach((inboundStats) => { + if ( + inboundStats && + typeof inboundStats === "object" && + "audioLevel" in inboundStats && + typeof inboundStats.audioLevel === "number" + ) { + if (inboundStats.audioLevel > audioLevelThreshold) { + isAudioLevelAboveThreshold = true; + } + } + }); + } + + // If no incoming audio, check if we have local audio above a certain threshold + if (!isAudioLevelAboveThreshold && mediaSourceStats.length) { + mediaSourceStats.forEach((sourceStats) => { + if ( + sourceStats && + typeof sourceStats === "object" && + "audioLevel" in sourceStats && + typeof sourceStats.audioLevel === "number" + ) { + if (sourceStats.audioLevel > audioLevelThreshold) { + isAudioLevelAboveThreshold = true; + } + } + }); + } + + dispatch({ + type: "AUDIO_LEVEL_ABOVE_THRESHOLD", + payload: isAudioLevelAboveThreshold, + }); + }); + }, 100); + + return () => { + window.clearInterval(statsInterval); + }; +}; diff --git a/src/components/production-line/use-rtc-connection.ts b/src/components/production-line/use-rtc-connection.ts index 2cf54579..051fe042 100644 --- a/src/components/production-line/use-rtc-connection.ts +++ b/src/components/production-line/use-rtc-connection.ts @@ -12,6 +12,7 @@ import { TJoinProductionOptions } from "./types.ts"; import { useGlobalState } from "../../global-state/context-provider.tsx"; import { TGlobalStateAction } from "../../global-state/global-state-actions.ts"; import { TUseAudioInputValues } from "./use-audio-input.ts"; +import { startRtcStatInterval } from "./rtc-stat-interval.ts"; type TRtcConnectionOptions = { inputAudioStream: TUseAudioInputValues; @@ -191,11 +192,18 @@ const establishConnection = ({ }); }); + const rtcStatIntervalTeardown = startRtcStatInterval({ + rtcPeerConnection, + dispatch, + }); + return { teardown: () => { dataChannel.removeEventListener("message", onDataChannelMessage); rtcPeerConnection.removeEventListener("track", onRtcTrack); + + rtcStatIntervalTeardown(); }, }; }; diff --git a/src/components/production-line/user-list.tsx b/src/components/production-line/user-list.tsx index 0900da82..01a61437 100644 --- a/src/components/production-line/user-list.tsx +++ b/src/components/production-line/user-list.tsx @@ -8,7 +8,6 @@ type TUserProps = { isYou: boolean; isActive: boolean; isTalking: boolean; - isDominant: boolean; }; const User = styled.div` @@ -25,8 +24,8 @@ const User = styled.div` ${({ isActive }) => `border-left: 1rem solid ${isActive ? "#7be27b;" : "#ebca6a;"}`} - ${({ isDominant }) => - `border-bottom: 0.5rem solid ${isDominant ? "#7be27b;" : "#353434;"}`} + ${({ isTalking }) => + `border-bottom: 0.5rem solid ${isTalking ? "#7be27b;" : "#353434;"}`} ${({ isTalking }) => isTalking @@ -42,11 +41,14 @@ type TUserListOptions = { participants: TParticipant[]; sessionid: string | null; dominantSpeaker: string | null; + audioLevelAboveThreshold: boolean; }; + export const UserList = ({ participants, sessionid, dominantSpeaker, + audioLevelAboveThreshold, }: TUserListOptions) => { if (!participants) return null; @@ -58,9 +60,9 @@ export const UserList = ({ key={p.sessionid} isYou={p.sessionid === sessionid} isActive={p.isActive} - isDominant={p.endpointid === dominantSpeaker} - // TODO connect to rtc data channel to get talking session id - isTalking={false} + isTalking={ + audioLevelAboveThreshold && p.endpointid === dominantSpeaker + } > {p.name} {p.isActive ? "" : "(inactive)"}