Awaited promise for navigator.mediaDevices.getUserMedia neither resolves or throws an error

35 Views Asked by At

I've built a react-native app using expo. The app is nothing but a webview for a responsive website.

The website in the webview needs to use the microphone sometimes to record (ideally the camera as well, but trying to figure this one out for now). The functionality works just fine on web but I am experiencing issues on the app. The permission is never granted/resolved, even though I do grant it when the app itself asks for the permission. The thing is that it doesn't seem to also fail? I've tried adding logs to understand at what point it fails and why, but even they don't print fully.

Here is my app (this is all there is to it)

import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { StyleSheet } from "react-native";
import { WebView } from "react-native-webview";

export default function App() {

  const debugging = `
  const consoleLog = (type, log) => window.ReactNativeWebView.postMessage(JSON.stringify({'type': 'Console', 'data': {'type': type, 'log': log}}));
  console = {
      log: (log) => consoleLog('log', log),
      debug: (log) => consoleLog('debug', log),
      info: (log) => consoleLog('info', log),
      warn: (log) => consoleLog('warn', log),
      error: (log) => consoleLog('error', log),
    };
`;

 const onMessage = (payload: any) => {
    let dataPayload;
    try {
      dataPayload = JSON.parse(payload.nativeEvent.data);
    } catch (e: unknown) {
      let message = "Parsing failed: ";
      if (e instanceof Error) message += e.message;
      console.log(message);
    }

    if (dataPayload) {
      if (dataPayload.type === "Console") {
        console.info(`[Console] ${JSON.stringify(dataPayload.data)}`);
      } else {
        console.log(dataPayload);
      }
    } else {
      console.log("No payload");
    }
  };

  return (
    <SafeAreaProvider>
      <SafeAreaView style={styles.container}>
        <WebView
          source={{ uri: "https://web.mynders.com" }}
          mediaCapturePermissionRequestType="grant"
          injectedJavaScript={debugging}
          onMessage={onMessage}
          style={styles.webView}
        />
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    borderWidth: 0,
  },
  webView: {
    flex: 1,
    borderWidth: 0,
  },
});

Then in my web app, here's the part that is trying to get the permissions:

import React, { useState } from "react";
import { PiMicrophoneSlashLight } from "react-icons/pi";

const RecordButton: React.FC = () => {
  const [permission, setPermission] = useState(false);

  const getMicrophonePermission = async () => {
    if ("MediaRecorder" in window) {
      try {
        console.log("trying to get permissions.");

        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
        });
        console.log("received permissions.");

        setPermission(true);
        console.log("Prmission was set to true.");

        stream.getTracks().forEach((t) => t.stop());
        console.log("cleared all tracks.");
      } catch (err: unknown) {
        let message = "Failed to get mic permissions: ";
        if (err instanceof Error) message += err.message;
        console.log(message);
        alert(message);
      }
    } else {
      console.log("The MediaRecorder API is not supported in your browser.");
      alert("The MediaRecorder API is not supported in your browser.");
    }
  };

  if (!permission)
    return (
      <div
        onClick={getMicrophonePermission}
        className={[
          "rounded-full cursor-pointer m-1 transition-all ease-in-out bg-gray-200 p-2",
        ].join(" ")}
      >
        <PiMicrophoneSlashLight className="h-5 w-5 text-gray-400 select-none cursor-pointer" />
      </div>
    );
};

export default RecordButton;

When I click the button, this is the log I get:

 INFO  [Console] {"type":"log","log":"trying to get."}

Nothing after to indicate either success or failure. It's as if it never finishes awaiting? Or something else, not sure, can't remember ever experiencing something like that.

This is my app.json btw with the permissions I ask for (currently trying to make it work on an Android device):

{
  "expo": {
    ...
    "android": {
      ...
      "permissions": [
        "android.permission.INTERNET",
        "android.permission.READ_EXTERNAL_STORAGE",
        "android.permission.WRITE_EXTERNAL_STORAGE",
        "android.permission.MODIFY_AUDIO_SETTINGS",
        "android.permission.AUDIO_CAPTURE",

        "android.permission.CAMERA",
        "android.permission.RECORD_AUDIO",
        "android.permission.ACCESS_FINE_LOCATION",
        "android.permission.ACCESS_COARSE_LOCATION",
        "android.permission.FOREGROUND_SERVICE"
      ]
    },
  }
}
1

There are 1 best solutions below

0
Tsabary On

I can't report on why I got no logs out, but as far as my oiginal issue of gting the permissions needed for the mic and camera, using expo-av and expo-camera I have it working now.

Here's the complete setup I have for the component:

import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { StyleSheet } from "react-native";
import { WebView } from "react-native-webview";
import { Audio } from "expo-av";
import { Camera } from "expo-camera";
import { useEffect, useState } from "react";

export default function App() {
  const [requestingAudio, setRequestingAudio] = useState(true);
  const [requestingCamera, setRequestingCamera] = useState(true);

  const debugging = `
  const consoleLog = (type, log) => window.ReactNativeWebView.postMessage(JSON.stringify({'type': 'Console', 'data': {'type': type, 'log': log}}));
  console = {
      log: (log) => consoleLog('log', log),
      debug: (log) => consoleLog('debug', log),
      info: (log) => consoleLog('info', log),
      warn: (log) => consoleLog('warn', log),
      error: (log) => consoleLog('error', log),
    };
`;

  const onMessage = (payload: any) => {
    let dataPayload;
    try {
      dataPayload = JSON.parse(payload.nativeEvent.data);
    } catch (e: unknown) {
      let message = "Parsing failed: ";
      if (e instanceof Error) message += e.message;
      console.log(message);
    }

    if (dataPayload) {
      if (dataPayload.type === "Console") {
        console.info(`[Console] ${JSON.stringify(dataPayload.data)}`);
      } else {
        console.log(dataPayload);
      }
    } else {
      console.log("No payload");
    }
  };

  async function requestAudioPermissions() {
    try {
      return await Audio.requestPermissionsAsync();
    } catch (err) {
      console.error("Failed to request permissions", err);
    } finally {
      setRequestingAudio(false);
    }
  }

  async function getAudioPermissions() {
    try {
      return await Audio.getPermissionsAsync();
    } catch (err) {
      console.error("Failed to get permissions", err);
    } finally {
      setRequestingAudio(false);
    }
  }

  async function requestCameraPermissions() {
    try {
      return await Camera.requestCameraPermissionsAsync();
    } catch (err) {
      console.error("Failed to get permission", err);
    } finally {
      setRequestingCamera(false);
    }
  }

  async function getCameraPermissions() {
    try {
      return await Camera.getCameraPermissionsAsync();
    } catch (err) {
      console.error("Failed to get permission", err);
    } finally {
      setRequestingCamera(false);
    }
  }

  useEffect(() => {
    (async () => {
      const permissions = await getAudioPermissions();
      if (permissions?.granted) {
        setRequestingAudio(false);
      } else {
        requestAudioPermissions();
      }
    })();
  }, []);

  useEffect(() => {
    if (requestingAudio) return;

    (async () => {
      const permissions = await getCameraPermissions();
      if (permissions?.granted) {
        setRequestingCamera(false);
      } else {
        requestCameraPermissions();
      }
    })();
  }, [requestingAudio]);

  if (requestingCamera) return null;

  return (
    <SafeAreaProvider>
      <SafeAreaView style={styles.container}>
        <WebView
          source={{ uri: "https://web.mynders.com" }}
          mediaCapturePermissionRequestType="grant"
          injectedJavaScript={debugging}
          onMessage={onMessage}
          style={styles.webView}
        />
      </SafeAreaView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    borderWidth: 0,
  },
  webView: {
    flex: 1,
    borderWidth: 0,
  },
});