webview_flutter
查看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),),);}
}
相关文章:
webview_flutter
查看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实例的解决方案&…...

【GESP考级C++】1级样题 闰年统计
GSEP 1级样题 闰年统计 题目描述 小明刚刚学习了如何判断平年和闰年,他想知道两个年份之间(包含起始年份和终止年份)有几个闰年。你能帮帮他吗? 输入格式 输入一行,包含两个整数,分别表示起始年份和终止…...

CentOS密码重置
背景: 我有一个CentOS虚拟机,但是密码忘记了,偶尔记起可以重置密码,于是今天尝试记录一下,又因为我最近记性比较差,所以必须要记录一下。 过程: 1、在引导菜单界面(grubÿ…...

Tomcat Servlet
Tomcat & Servlet 一、What is “Tomcat”?二、 What is “Servlet”?1、HttpServlet2、HttpServletRequest3、HttpServletResponse 一、What is “Tomcat”? Tomcat 本质上是一个基于 TCP 协议的 HTTP 服务器。我们知道HTTP是一种应用层协议,是 HTTP 客户端…...

国庆day2---select实现服务器并发
select.c: #include <myhead.h>#define ERR_MSG(msg) do{\fprintf(stderr,"__%d__:",__LINE__);\perror(msg);\ }while(0)#define IP "192.168.1.3" #define PORT 8888int main(int argc, const char *argv[]) {//创建报式套接字socketi…...

Grafana 开源了一款 eBPF 采集器 Beyla
eBPF 的发展如火如荼,在可观测性领域大放异彩,Grafana 近期也发布了一款 eBPF 采集器,可以采集服务的 RED 指标,本文做一个尝鲜介绍,让读者有个大概了解。 eBPF 基础介绍可以参考我之前的文章《eBPF Hello world》。理…...

亲测可用国产GPT人工智能
分享一些靠谱、可用、可以白嫖的GPT大模型。配合大模型,工作效率都会极大提升。 清华大学ChatGLM 官网: 智谱清言中国版对话语言模型,与GLM大模型进行对话。https://chatglm.cn/开源的、支持中英双语的1300亿参数的对话语言模型࿰…...
适配器模式详解和实现(设计模式 四)
适配器模式将一个类的接口转换成客户端所期望的另一个接口,解决由于接口不兼容而无法进行合作的问题。 设计基本步骤 1. 创建目标接口(Target Interface),该接口定义了客户端所期望的方法。 2.创建被适配类(Adaptee…...

IDEA的使用
文章目录 1.IDEA配置1.1 idea界面说明1.2 git1.3 JDK1.4 maven1.5 Tomcat1.6 idea设置编码格式1.7 vscodenodejs1.8 windows下安装redis 2. IDEA问题2.1 setAttribute方法爆红2.2 idea cannot download sources解决办法2.3 springboot项目跑起来不停run 3. vscode3.1 vscode显示…...

CSS详细基础(二)文本样式
插播一条CSS的工作原理: CSS是一种定义样式结构如字体、颜色、位置等的语言,被用于描述网页上的信息格式化和显示的方式。CSS样式可以直接存储于HTML网页或者单独的样式单文件。无论哪一种方式,样式单包含将样式应用到指定类型的元素的规则。…...

win10系统任务栏图标变成白色的解决办法
我平时都是用滴答清单进行管理这个自己的日程代办的,但是今天打开的时候发现这个快捷方式突然变成纯白色的了,重启电脑之后,这个图标的样式仍然没有变化。上网查找解决办法之后,终于搞好了,于是就有了下面的教程。 为什…...
hadoop生态现状、介绍、部署
一、引出hadoop 1、hadoop的高薪现状 各招聘平台都有许多hadoop高薪职位,可以看看职位所需求的技能 ----> hadoop是什么,为什么会这么高薪?引出大数据,大数据时代,大数据与云计算 2、大数据时代的介绍 大数据的故事…...
二、EFCore 数据库表的创建和迁移
文章目录 一、数据库连接二、数据库表迁移一、数据库连接 在NuGet上安装EntityFramework 代码如下: Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.SqlServerMicrosoft.Extensions.Configuration.Json配置数据连接 appsettings.json 增加数据库连接配置 &quo…...
在nodejs中使用typescript
在nodejs中使用typescript 如果我们正在使用nodejs来开发应用,那么对于管理和扩展一个大型代码库来说是一个非常大的挑战。克服这一问题的方法之一是使用typescript,为js添加可选的类型注释和高级功能。在本文中,我们将探讨如何使用在nodejs中使用types…...

数据结构与算法基础(青岛大学-王卓)(8)
哎呀呀,sorry艾瑞波地,这次真的断更一个月了,又发生了很多很多事情,秋风开始瑟瑟了,老父亲身体查出肿瘤了,有病请及时就医,愿每一个人都有一个健康的身体,God bless U and FAMILY. 直…...

【生物信息学】使用谱聚类(Spectral Clustering)算法进行聚类分析
目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 3. IDE 三、实验内容 0. 导入必要的工具 1. 生成测试数据 2. 绘制初始数据分布图 3. 循环尝试不同的参数组合并计算聚类效果 4. 输出最佳参数组合 5. 绘制最佳聚类结果图 6. 代码整合 一、实验介绍…...

CSS基础语法第二天
目录 一、复合选择器 1.1 后代选择器 1.2 子代选择器 1.3 并集选择器 1.4 交集选择器 1.4.1超链接伪类 二、CSS特性 2.1 继承性 2.2 层叠性 2.3 优先级 基础选择器 复合选择器-叠加 三、Emmet 写法 3.1HTML标签 3.2CSS 四、背景属性 4.1 背景图 4.2 平铺方式 …...

ThreeJS - 封装一个GLB模型展示组件(TypeScript)
一、引言 最近基于Three.JS,使用class封装了一个GLB模型展示,支持TypeScript、支持不同框架使用,具有多种功能。 (下图展示一些基础的功能,可以自行扩展,比如光源等) 二、主要代码 本模块依赖…...
HashMap面试题
1.hashMap底层实现 hashMap的实现我们是要分jdk 1.7及以下版本,jdk1.8及以上版本 jdk 1.7 实现是用数组链表 jdk1.8 实现是用数组链表红黑树, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,…...

Java编程技巧:swagger2、knif4j集成SpringBoot或者SpringCloud项目
目录 1、springbootswagger2knif4j2、springbootswagger3knif4j3、springcloudswagger2knif4j 1、springbootswagger2knif4j 2、springbootswagger3knif4j 3、springcloudswagger2knif4j 注意点: Api注解:Controller类上的Api注解需要添加tags属性&a…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...

GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...

Qt的学习(一)
1.什么是Qt Qt特指用来进行桌面应用开发(电脑上写的程序)涉及到的一套技术Qt无法开发网页前端,也不能开发移动应用。 客户端开发的重要任务:编写和用户交互的界面。一般来说和用户交互的界面,有两种典型风格&…...
起重机起升机构的安全装置有哪些?
起重机起升机构的安全装置是保障吊装作业安全的关键部件,主要用于防止超载、失控、断绳等危险情况。以下是常见的安全装置及其功能和原理: 一、超载保护装置(核心安全装置) 1. 起重量限制器 功能:实时监测起升载荷&a…...