查看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 中声明的是这样的
<key>NSCameraUsageDescription</key><string></string>
app内申请权限 依旧可以正常使用 并获取到,但是webview不行
<key>NSCameraUsageDescription</key>
<string>Konnect wants to use your camera, is that allowed?</string>
原因是<string></string> =》
对flutter app请求权限做了如下封装<string>Konnect wants to use your camera, is that allowed?</string>
权限虽然声明了 但是,没有说明权限的使用用途,这个描述提示会 展示在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<bool> 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<bool> 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,});@overrideState<CommonWebView> createState() => _CommonWebViewState();
}class _CommonWebViewState extends State<CommonWebView> {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;@overridevoid initState() {webViewInit();super.initState();}webViewInit() {if (WebViewPlatform.instance is WebKitWebViewPlatform) {params = WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true,mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},);} 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-100this.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<String, dynamic> msg = json.decode(sendMesStr);int type = msg["type"] ?? -1;String method = msg["method"] ?? "";Map<String, dynamic> 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<Map<String, dynamic>>(result: data);break;case "getWalletAddress":WalletEntity? myWallet = await WalletSql.getDefaultWallet();if (myWallet != null) {String walletAddress = myWallet.walletAddress;String signature = myWallet.signature ?? "";controller.runJavaScript("appCallMethod('walletAddress','$walletAddress')");controller.runJavaScript("appCallMethod('signature','$signature')");KLogUtil.log(["获取到钱包", walletAddress, signature]);} else {KLogUtil.log(["没有获取到钱包"]);}default:break;}}}backPress() async {Get.back();return;// 有问题// String? cur = await _webControl.currentUrl();// KLogUtil.log(["backPress", initUrl, cur]);// if (initUrl == cur) {// // 最后一页// KLogUtil.log(["最后一页"]);// Get.back();// }// if (await _webControl.canGoBack()) {// _webControl.goBack(); //返回上个网页// return false;// }// Get.back();}@overrideWidget build(BuildContext context) {return SafeArea(child: Scaffold(backgroundColor: AppTheme.themeColor_black,appBar: widget.isHiddenBar? null: WebViewTopAppBar(title: widget.title, backPress: backPress),body: WebViewWidget(controller: controller),),);}
}class PageError404 extends StatefulWidget {const PageError404({super.key});@overrideState<PageError404> createState() => _PageError404State();
}class WebViewTopAppBar extends PreferredSize {WebViewTopAppBar({key,required String title, //标题final Color? titleColor, //title颜色final Color? backgroundColor, //导航栏背景颜色final Widget? leadWidget, //左边按钮final PreferredSizeWidget? bottom, //底部组件final double? elevation, //AppBar底部阴影效果,0为无阴影。final bool statusBarColor = true, //控制状态栏的颜色true为灰色,false为白色final List<Widget>? rightActions, //右边按钮组final double? preferredSize, //状态栏高度final double bottomHeight = 0.0, //appBar底部高度final TextStyle? titleStyle,Function()? backPress, //返回按钮回调函数}) : super(key: key,preferredSize: Size.fromHeight(44.h),child: AppBar(title: Text(title,style: titleStyle ??TextStyle(fontSize: 16.sp,fontWeight: FontWeight.bold,color: titleColor ?? Colors.white,),),centerTitle: true,leading: leadWidget ??InkWell(onTap: backPress,child: Padding(padding: EdgeInsets.all(6.r),child: Assets.icon.arrowBack.image(),),),actions: rightActions,bottom: bottom,elevation: elevation ?? 0,// titleTextStyle: Theme.of(context).textTheme.bodySmall,backgroundColor: backgroundColor ?? AppTheme.themeColor_black,systemOverlayStyle: SystemUiOverlayStyle(statusBarColor: Colors.transparent,statusBarBrightness:Platform.isAndroid ? Brightness.light : Brightness.dark,statusBarIconBrightness:Platform.isAndroid ? Brightness.light : Brightness.dark,systemNavigationBarIconBrightness: Brightness.light,systemNavigationBarColor: AppTheme.themeColor_black,),),);
}class _PageError404State extends State<PageError404> {@overrideWidget build(BuildContext context) {return SafeArea(child: Scaffold(backgroundColor: AppTheme.themeColor_black,appBar: TopAppBar(context, "404"),body: PlaceholderWidget.emptyPlaceholder(placeholderType: PlaceholderType.notFound404),),);}
}