查看webview内核
https://liulanmi.com/labs/core.html
h5中获取设备
https://cloud.tencent.com/developer/ask/sof/105938013
https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/mediaDevices
web资源部署后navigator获取不到mediaDevices实例的解决方案(navigator.mediaDevices为undefined)_navigator.mediadevices不存在_乐辞的博客-CSDN博客
自已遇到的坑:
需求是app进入webview 使用h5网页获取摄像头进行人脸识别。但是h5那边一直获取不到摄像头权限
H5 页面通过navigator.mediaDevices.getUserMedia调用手机摄像头拍照上传_navigator.webkitgetusermedia-CSDN博客
web资源部署后navigator获取不到mediaDevices实例的解决方案(navigator.mediaDevices为undefined)_navigator.mediadevices不存在_乐辞的博客-CSDN博客
h5 navigator.mediaDevices 一直是undefined
原因1: 由于浏览器的安全策略导致了这个问题,目前经尝试,在以下几种情况中 navigator.mediaDevices 可以正常使用
1. 地址为localhost:// 访问时 2. 地址为https:// 时 3. 为文件访问file:///
原因2:排除上面的问题, 使用了navigator兼容性写法获取 getUserMedia 摄像头设备 但是 getUserMedia 依旧为underfined--》怀疑是app的webview 的问题
怀疑是webview版本问题
解决方式1: 我将webview升级到最新版 发现问题不是webview的版本问题❌
最后发现flutter app中 申请权限 使用 permission_handler: ^10.3.0
await Permission.camera.request() ->
if (await KPermiseeUtil.checkAndDoDefault(
Permission.camera)) {
_callCamera();
}
安卓 谷歌内核:-》方向权限在安卓声明文件 AndroidManifest.xml 什么了对应权限 app内权限是有的。那说明只是webview的权限问题
安卓权限配置 和webview权限配置
android - How to access the camera from within a Webview? - Stack Overflow
按照上面的 依旧没有解决
最后发现 webview有个权限申请
controller.platform.setOnPlatformPermissionRequest
虽然app 进入webview申请了权限 方式 webview内部也需要进行权限申请
默认setOnPlatformPermissionRequest 这个函数回调是拒绝的 所以加下面的配置 解决
安卓不能使用h5打开摄像头的问题。
controller.platform.setOnPlatformPermissionRequest(
(request) {
request.grant();
},
);
苹果 wkwebview的内核
如果开始有权限 流程正常,如果开始app没有权限 申请权限后 webview 依旧没有权限 需要退出app重新进才会有权限
原因解决:
之前在ios的info.plist 中声明的是这样的
NSCameraUsageDescription
app内申请权限 依旧可以正常使用 并获取到,但是webview不行
NSCameraUsageDescription
Konnect wants to use your camera, is that allowed?
原因是
对flutter app请求权限做了如下封装
权限虽然声明了 但是,没有说明权限的使用用途,这个描述提示会 展示在app申请权限的弹窗底部
加上了 就解决了 上面的问题:
注意:
ios权限声明 必须添加描述 权限使用来干什么 不然app上架会被拒绝。
NSCameraUsageDescription_camera usage d-CSDN博客
import 'dart:io';
import 'package:app/common/util/k_log_util.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class KPermiseeUtil {
static final bool isIos = Platform.isIOS;
static final bool isAndroid = Platform.isAndroid;
// 检查权限 并做相应的处理
static Future checkAndDoDefault(Permission permission) async {
final status = await permission.status;
KLogUtil.log(["status", status]);
if (isIos) {
switch (status) {
case PermissionStatus.granted:
return true;
case PermissionStatus.permanentlyDenied:
openAppSettings();
return false;
case PermissionStatus.denied:
case PermissionStatus.limited:
case PermissionStatus.restricted:
default:
final newStatus = await permission.request();
if (newStatus == PermissionStatus.granted) {
return true;
}
return false;
}
} else if (isAndroid) {
switch (status) {
case PermissionStatus.granted:
return true;
default:
final newStatus = await permission.request();
KLogUtil.log(["newStatus", newStatus]);
if (newStatus == PermissionStatus.granted) {
return true;
} else if (newStatus == PermissionStatus.denied) {
return false;
} else if (newStatus == PermissionStatus.permanentlyDenied) {
openAppSettings();
return false;
}
return false;
}
}
return await permission.status == PermissionStatus.granted;
}
static Future checkStatusAndDoDefault(
PermissionState status, Permission permission) async {
KLogUtil.log(["status", status]);
if (isIos) {
switch (status) {
// notDetermined 未设置授权
case PermissionState.notDetermined:
return true;
// 该应用程序未被授权访问照片库,用户也无法授予此类权限。
case PermissionState.restricted:
openAppSettings();
return false;
// 用户明确拒绝此应用程序访问照片库。
case PermissionState.denied:
// 用户明确授予此应用程序访问照片库的权限。
case PermissionState.authorized:
case PermissionState.limited:
default:
final newStatus = await permission.request();
if (newStatus == PermissionStatus.granted) {
return true;
}
return false;
}
} else if (isAndroid) {
switch (status) {
case PermissionStatus.granted:
return true;
default:
final newStatus = await permission.request();
KLogUtil.log(["newStatus", newStatus]);
if (newStatus == PermissionStatus.granted) {
return true;
} else if (newStatus == PermissionStatus.denied) {
return false;
} else if (newStatus == PermissionStatus.permanentlyDenied) {
openAppSettings();
return false;
}
return false;
}
}
return await permission.status == PermissionStatus.granted;
}
}
最后自己的封装如下 (最新版本webview)
webview_flutter: ^4.2.4
webview_flutter_android: ^3.10.1
webview_flutter_wkwebview: ^3.7.4
import 'dart:convert';
import 'dart:io';
import 'package:app/common/theme/app_theme.dart';
import 'package:app/common/util/k_log_util.dart';
import 'package:app/entity/wallet/wallet_entity.dart';
import 'package:app/gen/assets.gen.dart';
import 'package:app/pages/widget/placeholder_widget.dart';
import 'package:app/pages/widget/top_appbar.dart';
import 'package:app/sql/wallet_sql/wallet_sql.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'placeholder_type.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
// ignore: must_be_immutable
class CommonWebView extends StatefulWidget {
final String title;
final String url;
bool isHiddenBar;
CommonWebView({
super.key,
required this.title,
required this.url,
this.isHiddenBar = false,
});
@override
State createState() => _CommonWebViewState();
}
class _CommonWebViewState extends State {
late double progress = 0.01; //H5加载进度值
final double VISIBLE = 2;
final double GONE = 0;
late double progressHeight = GONE; //H5加载进度条高度
late WebViewController controller;
late final PlatformWebViewControllerCreationParams params;
@override
void initState() {
webViewInit();
super.initState();
}
webViewInit() {
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const {},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
controller = WebViewController.fromPlatformCreationParams(params);
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
controller.platform.setOnPlatformPermissionRequest(
(request) {
request.grant();
},
);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel("konnect", onMessageReceived: onMessageReceived)
..setBackgroundColor(AppTheme.themeColor_black)
..enableZoom(true)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
// Update loading bar.
//加载H5页面时触发多次,progress值为0-100
this.progress = progress.toDouble() / 100.0; //计算成0.0-1.0之间的值
},
onPageStarted: (String url) {
//H5页面开始加载时触发
setProgressVisible(GONE); //VISIBLE 显示加载进度条
},
onPageFinished: (String url) {
//H5页面加载完成时触发
setProgressVisible(GONE); //隐藏加载进度条
},
onWebResourceError: (WebResourceError error) {
KLogUtil.log(["error", error]);
if (error.isForMainFrame == true) {
switch (error.errorType) {
case WebResourceErrorType.badUrl:
case WebResourceErrorType.timeout:
break;
case WebResourceErrorType.hostLookup:
Get.off(() => const PageError404());
default:
break;
}
}
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadRequest(Uri.parse(widget.url));
}
//显示或隐藏进度条
void setProgressVisible(double isVisible) {
setState(() {
progressHeight = isVisible;
});
}
// 接受h5发送来的数据
onMessageReceived(message) async {
//接收H5发过来的数据
String sendMesStr = message.message;
print("H5发过来的数据1: $sendMesStr");
KLogUtil.log(["H5发过来的数据1", sendMesStr]);
Map msg = json.decode(sendMesStr);
int type = msg["type"] ?? -1;
String method = msg["method"] ?? "";
Map data = msg["data"] ?? {};
if (type != -1) {
switch (type) {
case 1:
break;
case 2:
break;
case 3:
default:
break;
}
}
if (method.isNotEmpty) {
switch (method) {
case "back":
KLogUtil.log(["back", data, msg]);
Get.back