You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo React Native WebView中getUserMedia Promise无响应问题求助

问题:Expo React Native WebView中getUserMedia无响应(麦克风权限)

我用Expo构建了一个React Native应用,它仅作为响应式网站的WebView容器。网页端调用麦克风录音功能正常,但在App端出现异常:即使授权了权限,navigator.mediaDevices.getUserMedia的Promise既不resolve也不reject,日志只打印出"trying to get permissions.",后续无任何成功或失败的输出。


React Native应用代码

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,
  },
});

Web应用权限请求代码

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;

日志输出

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

app.json权限配置(Android)

{
  "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. 先通过Expo API获取原生权限

WebView的mediaCapturePermissionRequestType="grant"无法自动处理原生权限流程,必须先通过Expo的权限API主动获取录音权限,再渲染WebView:

import { useEffect, useState } from 'react';
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { StyleSheet, Alert } from "react-native";
import { WebView } from "react-native-webview";
import * as Permissions from 'expo-permissions';

export default function App() {
  const [hasAudioPermission, setHasAudioPermission] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const requestPermissions = async () => {
      const { status } = await Permissions.askAsync(Permissions.AUDIO_RECORDING);
      if (status === 'granted') {
        setHasAudioPermission(true);
      } else {
        Alert.alert('权限不足', '需要录音权限才能使用该功能');
      }
      setLoading(false);
    };
    requestPermissions();
  }, []);

  // 保留原有debugging和onMessage逻辑...

  if (loading) {
    return <SafeAreaView style={styles.container} />; // 加载占位
  }

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

// 保留原有styles...

2. 精简并修正权限配置

移除冗余权限,确保核心权限正确,同时补充iOS的权限描述(必填):

{
  "expo": {
    ...
    "android": {
      ...
      "permissions": [
        "android.permission.INTERNET",
        "android.permission.RECORD_AUDIO",
        "android.permission.MODIFY_AUDIO_SETTINGS"
      ]
    },
    "ios": {
      ...
      "infoPlist": {
        "NSMicrophoneUsageDescription": "需要访问麦克风进行音频录制"
      }
    }
  }
}

3. 添加WebView权限回调监听

手动处理WebView的权限请求事件,确保权限状态同步:

<WebView
  ...
  onMediaCapturePermissionRequest={(event) => {
    if (hasAudioPermission) {
      event.url && event.grant(true);
    } else {
      event.url && event.grant(false);
      Alert.alert('需要先授予录音权限');
    }
  }}
/>

4. 测试真机环境

Expo Go的沙箱环境可能限制媒体权限,建议构建自定义开发客户端或直接打包APK/IPA测试,避免环境限制导致的异常。


内容的提问来源于stack exchange,提问作者Tsabary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 01:55:57