Fix double audio tracks

See comments. I'm not very happy with how this code bounces state in and out of different hooks and useEffect blocks, but as a quick fix this should work.
This commit is contained in:
Robin
2023-09-20 13:21:45 -04:00
parent fb4e0784fc
commit 69bf3bd9a1
2 changed files with 48 additions and 28 deletions

View File

@@ -17,10 +17,8 @@ limitations under the License.
import { import {
AudioCaptureOptions, AudioCaptureOptions,
ConnectionState, ConnectionState,
LocalTrackPublication,
Room, Room,
RoomEvent, RoomEvent,
Track,
} from "livekit-client"; } from "livekit-client";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -56,24 +54,22 @@ async function doConnect(
audioOptions: AudioCaptureOptions audioOptions: AudioCaptureOptions
): Promise<void> { ): Promise<void> {
await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt); await livekitRoom!.connect(sfuConfig!.url, sfuConfig!.jwt);
const hasMicrophoneTrack = Array.from(
livekitRoom?.localParticipant.audioTracks.values()
).some((track: LocalTrackPublication) => {
return track.source == Track.Source.Microphone;
});
// We create a track in case there isn't any.
if (!hasMicrophoneTrack) {
const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions,
});
if (audioTracks.length < 1) {
logger.info("Tried to pre-create local audio track but got no tracks");
return;
}
if (!audioEnabled) await audioTracks[0].mute();
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]); // Always create an audio track manually.
// livekit (by default) keeps the mic track open when you mute, but if you start muted,
// doesn't publish it until you unmute. We want to publish it from the start so we're
// always capturing audio: it helps keep bluetooth headsets in the right mode and
// mobile browsers to know we're doing a call.
const audioTracks = await livekitRoom!.localParticipant.createTracks({
audio: audioOptions,
});
if (audioTracks.length < 1) {
logger.info("Tried to pre-create local audio track but got no tracks");
return;
} }
if (!audioEnabled) await audioTracks[0].mute();
await livekitRoom?.localParticipant.publishTrack(audioTracks[0]);
} }
export function useECConnectionState( export function useECConnectionState(
@@ -89,6 +85,7 @@ export function useECConnectionState(
); );
const [isSwitchingFocus, setSwitchingFocus] = useState(false); const [isSwitchingFocus, setSwitchingFocus] = useState(false);
const [isInDoConnect, setIsInDoConnect] = useState(false);
const onConnStateChanged = useCallback((state: ConnectionState) => { const onConnStateChanged = useCallback((state: ConnectionState) => {
if (state == ConnectionState.Connected) setSwitchingFocus(false); if (state == ConnectionState.Connected) setSwitchingFocus(false);
@@ -125,12 +122,17 @@ export function useECConnectionState(
(async () => { (async () => {
setSwitchingFocus(true); setSwitchingFocus(true);
await livekitRoom?.disconnect(); await livekitRoom?.disconnect();
await doConnect( setIsInDoConnect(true);
livekitRoom!, try {
sfuConfig!, await doConnect(
initialAudioEnabled, livekitRoom!,
initialAudioOptions sfuConfig!,
); initialAudioEnabled,
initialAudioOptions
);
} finally {
setIsInDoConnect(false);
}
})(); })();
} else if ( } else if (
!sfuConfigValid(currentSFUConfig.current) && !sfuConfigValid(currentSFUConfig.current) &&
@@ -142,16 +144,24 @@ export function useECConnectionState(
// doesn't publish it until you unmute. We want to publish it from the start so we're // doesn't publish it until you unmute. We want to publish it from the start so we're
// always capturing audio: it helps keep bluetooth headsets in the right mode and // always capturing audio: it helps keep bluetooth headsets in the right mode and
// mobile browsers to know we're doing a call. // mobile browsers to know we're doing a call.
setIsInDoConnect(true);
doConnect( doConnect(
livekitRoom!, livekitRoom!,
sfuConfig!, sfuConfig!,
initialAudioEnabled, initialAudioEnabled,
initialAudioOptions initialAudioOptions
); ).finally(() => setIsInDoConnect(false));
} }
currentSFUConfig.current = Object.assign({}, sfuConfig); currentSFUConfig.current = Object.assign({}, sfuConfig);
}, [sfuConfig, livekitRoom, initialAudioOptions, initialAudioEnabled]); }, [sfuConfig, livekitRoom, initialAudioOptions, initialAudioEnabled]);
return isSwitchingFocus ? ECAddonConnectionState.ECSwitchingFocus : connState; // Because we create audio tracks by hand, there's more to connecting than
// just what LiveKit does in room.connect, and we should continue to return
// ConnectionState.Connecting for the entire duration of the doConnect promise
return isSwitchingFocus
? ECAddonConnectionState.ECSwitchingFocus
: isInDoConnect
? ConnectionState.Connecting
: connState;
} }

View File

@@ -23,7 +23,7 @@ import {
setLogLevel, setLogLevel,
} from "livekit-client"; } from "livekit-client";
import { useLiveKitRoom } from "@livekit/components-react"; import { useLiveKitRoom } from "@livekit/components-react";
import { useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import E2EEWorker from "livekit-client/e2ee-worker?worker"; import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@@ -98,6 +98,11 @@ export function useLiveKit(
[e2eeOptions] [e2eeOptions]
); );
// useECConnectionState creates and publishes an audio track by hand. To keep
// this from racing with LiveKit's automatic creation of the audio track, we
// block audio from being enabled until the connection is finished.
const [blockAudio, setBlockAudio] = useState(true);
// We have to create the room manually here due to a bug inside // We have to create the room manually here due to a bug inside
// @livekit/components-react. JSON.stringify() is used in deps of a // @livekit/components-react. JSON.stringify() is used in deps of a
// useEffect() with an argument that references itself, if E2EE is enabled // useEffect() with an argument that references itself, if E2EE is enabled
@@ -105,7 +110,7 @@ export function useLiveKit(
const { room } = useLiveKitRoom({ const { room } = useLiveKitRoom({
token: sfuConfig?.jwt, token: sfuConfig?.jwt,
serverUrl: sfuConfig?.url, serverUrl: sfuConfig?.url,
audio: initialMuteStates.current.audio.enabled, audio: initialMuteStates.current.audio.enabled && !blockAudio,
video: initialMuteStates.current.video.enabled, video: initialMuteStates.current.video.enabled,
room: roomWithoutProps, room: roomWithoutProps,
connect: false, connect: false,
@@ -120,6 +125,11 @@ export function useLiveKit(
sfuConfig sfuConfig
); );
// Unblock audio once the connection is finished
useEffect(() => {
if (connectionState === ConnectionState.Connected) setBlockAudio(false);
}, [connectionState, setBlockAudio]);
useEffect(() => { useEffect(() => {
// Sync the requested mute states with LiveKit's mute states. We do it this // Sync the requested mute states with LiveKit's mute states. We do it this
// way around rather than using LiveKit as the source of truth, so that the // way around rather than using LiveKit as the source of truth, so that the