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…...
Step3-VL-10B在重装系统后的快速部署方案:一键恢复AI环境
Step3-VL-10B在重装系统后的快速部署方案:一键恢复AI环境 系统重装后最头疼的就是环境配置?别担心,这份指南让你10分钟恢复AI开发环境 刚重装完系统,看着空空如也的开发环境,是不是有点无从下手?特别是像St…...
java中遍历map里面的键值对
代码示例:import java.util.HashMap; import java.util.Map;public class MapEntryDemo {public static void main(String[] args) {// 1. 创建一个Map(字典)Map<String, Integer> scoreMap new HashMap<>();scoreMap.put("…...
计算机网络相关知识
1. 计算机网络基础概念计算机网络是指通过通信设备和线路将地理位置不同的、具有独立功能的计算机系统连接起来,在网络软件的支持下实现资源共享和信息传递的系统。按照覆盖范围可分为:局域网(LAN):覆盖范围较小&#…...
VMware17.6.3安装包百度网盘
通过网盘分享的文件:VMware-workstation-full-17.6.3-24583834.exe 链接: https://pan.baidu.com/s/1PMvUQzIXBcCLl9MT1ehFDQ 提取码: wpmy --来自百度网盘超级会员v1的分享...
企业级CRM客户关系管理软件|ThinkPHP+FastAdmin开发|含源码+UniApp小程序/H5双端
温馨提示:文末有联系方式什么是企业级CRM客户关系管理系统 这是一款面向现代团队的智能客户关系管理(CRM)软件,采用成熟稳定的ThinkPHP框架与FastAdmin后台快速开发平台构建,兼顾高性能与易维护性,助力企业…...
数据库运维最佳实践
数据库运维最佳实践:保障数据安全与高效运行 在数字化时代,数据库作为企业核心数据的存储和管理平台,其稳定性和安全性直接影响业务连续性。高效的数据库运维不仅能提升系统性能,还能降低故障风险。本文将介绍数据库运维中的关键…...
Aleatoric vs Epistemic:用TensorFlow 2.x理解深度学习中的两种不确定性
Aleatoric vs Epistemic:用TensorFlow 2.x解析深度学习中的不确定性本质 在医疗影像诊断系统中,当AI模型对某张X光片标注"70%概率显示肿瘤"时,这个数字背后隐藏着怎样的信任度?这种不确定性究竟源于影像本身的模糊&…...
Qwen3-Reranker-8B效果展示:短视频脚本生成中多候选文案重排序
Qwen3-Reranker-8B效果展示:短视频脚本生成中多候选文案重排序 1. 引言:当AI遇上短视频创作 你有没有遇到过这样的场景?脑子里有个绝妙的短视频创意,一口气让AI生成了十几版文案,结果看着满屏的选项,反而…...
Conda环境创建报404错误?三步搞定清华镜像源配置问题
Conda环境创建报404错误?三步搞定清华镜像源配置问题 最近在帮团队新来的数据分析师配置开发环境时,遇到了一个典型问题:使用conda创建Python环境时频繁报404错误。这种问题在国内开发者中相当常见,尤其是当默认镜像源不稳定或配置…...
STC8A8K64D4 vs STC15:老司机带你选型,升级到8位机‘性能小钢炮’的5个理由
STC8A8K64D4 vs STC15:老司机带你选型,升级到8位机‘性能小钢炮’的5个理由 如果你还在用STC15系列单片机开发项目,现在可能是时候考虑升级了。STC8A8K64D4作为STC最新一代8位机,凭借其强悍的性能和丰富的外设,正在成为…...
