Skip to content

iOS/Expo: Weird behavior with setMicrophoneEnabled(true) #287

@imsamdez

Description

@imsamdez

Describe the bug

On iOS, toggling the microphone off and then back on with setMicrophoneEnabled consistently triggers:

  • Both iOS system sound for mic toggle/untoggle
  • A Metro bundler error:
Error: ENOENT: no such file or directory, open '/.../InternalBytecode.js'
    at Object.readFileSync (node:fs:448:20)
    at getCodeFrame (.../node_modules/metro/src/Server.js:997:18)
    ...
  • A warning from the RN event emitter:
WARN  An event listener wasn't added because it has been added already: bubble [Function anonymous]

To Reproduce

Steps to reproduce the behavior:

  1. Use a minimal component (see code sample below).
  2. Join a LiveKit room on iOS device.
  3. Tap Mute, then tap Unmute.
  4. Observe the duplicate sounds warning & Metro throwing InternalBytecode.js ENOENT.
// Minimal component to reproduce

import {
  LiveKitRoom,
  registerGlobals,
  useRoomContext,
} from "@livekit/react-native";
import { ConnectionState } from "livekit-client";
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

registerGlobals()

// Set this
const SERVER_URL = "to-set";
const TOKEN = "to-set";

export default function RoomPage() {
  const handleError = React.useCallback((e: unknown) => {
    console.warn("[LiveKitRoom error]", e);
  }, []);

  return (
    <LiveKitRoom
      serverUrl={SERVER_URL}
      token={TOKEN}
      connect={true}
      audio={true}
      video={false}
      onError={handleError}
    >
      <Content />
    </LiveKitRoom>
  );
}

function Content() {
  const room = useRoomContext();
  const state = room?.state ?? ConnectionState.Disconnected;

  const mute = async () => {
    try {
      await room?.localParticipant.setMicrophoneEnabled(false);
      console.log("[Mic] setMicrophoneEnabled(false) resolved");
    } catch (e) {
      console.error("[Mic] mute failed:", e);
    }
  };

  const unmute = async () => {
    try {
      await room?.localParticipant.setMicrophoneEnabled(true);
      console.log("[Mic] setMicrophoneEnabled(true) resolved");
    } catch (e) {
      console.error("[Mic] unmute failed:", e);
    }
  };

  return (
    <View style={s.container}>
      <Text style={s.h}>Minimal Mic Test</Text>
      <Text style={s.sub}>Connection: {state}</Text>
      <View style={s.row}>
        <Btn label="Mute" onPress={mute} />
        <Btn label="Unmute" onPress={unmute} />
      </View>
      <Text style={s.note}>
        Tip: ensure registerGlobals() is called once globally (not per refresh).
      </Text>
    </View>
  );
}

function Btn({
  label,
  onPress,
  disabled,
}: {
  label: string;
  onPress: () => void;
  disabled?: boolean;
}) {
  return (
    <Pressable
      onPress={onPress}
      disabled={disabled}
      style={[s.btn, disabled && s.btnDis]}
    >
      <Text style={s.btnTxt}>{label}</Text>
    </Pressable>
  );
}

const s = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#0f1115", padding: 16 },
  h: { color: "#fff", fontSize: 18, fontWeight: "700", marginBottom: 8 },
  sub: { color: "#9fb3c8", marginBottom: 16 },
  row: { flexDirection: "row", gap: 12 },
  btn: {
    backgroundColor: "#1f6feb",
    paddingHorizontal: 14,
    paddingVertical: 10,
    borderRadius: 10,
  },
  btnDis: { opacity: 0.5 },
  btnTxt: { color: "#fff", fontWeight: "600" },
  note: { color: "#9fb3c8", marginTop: 16, fontSize: 12 },
});

Expected behavior

  • Mic should disable and re-enable cleanly.
  • No Metro errors or duplicate listener warnings.

Device Info:

  • Device: iPhone 11
  • OS: 18.7
  • Using Expo

Dependencies Info (please reference your package-lock.json or yarn.lock file, not just your package.json):

  • @livekit/react-native: "^2.9.1"
  • livekit-client: "^2.15.7"
  • react-native-webrtc: "^137.0.2"

Additionnal information

I'm seeing the same behavior when turning off/on the camera.

Here is the log of my iOS device.

Noticed that when I click the unmute button, log with timestamp "15:54:39" appears and 2 seconds later, the two iOS system sounds happens and the log with timestamp 15:54:41 also appears.

15:54:39.220918+0200	livekitpoc	TX focusApplication (peekAppEvent) stealKB:Y scene:com.samdez.livekitpoc-default
15:54:39.220994+0200	livekitpoc	Evaluating dispatch of UIEvent: 0x11bc7dea0; type: 0; subtype: 0; backing type: 11; shouldSend: 1; ignoreInteractionEvents: 0, systemGestureStateChange: 0
15:54:39.221225+0200	livekitpoc	Sending UIEvent type: 0; subtype: 0; to windows: 1
15:54:39.221352+0200	livekitpoc	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x10694dfd0>; contextId: 0xA04E17AF
15:54:39.281480+0200	livekitpoc	Evaluating dispatch of UIEvent: 0x11bc7dea0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
15:54:39.346501+0200	livekitpoc	Evaluating dispatch of UIEvent: 0x11bc7dea0; type: 0; subtype: 0; backing type: 11; shouldSend: 1; ignoreInteractionEvents: 0, systemGestureStateChange: 0
15:54:39.346760+0200	livekitpoc	Sending UIEvent type: 0; subtype: 0; to windows: 1
15:54:39.346889+0200	livekitpoc	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x10694dfd0>; contextId: 0xA04E17AF
15:54:39.347243+0200	livekitpoc	Evaluating dispatch of UIEvent: 0x11bc7dea0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
15:54:39.348130+0200	livekitpoc	AVAudioSessionClient_Common.mm:526   Setting PrefersToInterruptActiveRecordingSessions to 0
15:54:41.305310+0200	livekitpoc	    AVAudioSession_iOS.mm:4906  Session 0x185b2e5 posting AVAudioSessionAudioHardwareFormatChangeNotification
15:54:41.305712+0200	livekitpoc	    AVAudioSession_iOS.mm:443   Session 0x185b2e5 posting AVAudioSessionRouteChangeNotification. Reason: AVAudioSessionRouteChangeReasonCategoryChange

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions