查看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
自已遇到的坑:
需求是app进入webview 使用h5网页获取摄像头进行人脸识别。但是h5那边一直获取不到摄像头权限
H5 页面通过navigator.mediaDevices.getUserMedia调用手机摄像头拍照上传_navigator.webkitgetusermedia-CSDN博客
h5 navigator.mediaDevices 一直是undefined
原因1: 由于浏览器的安全策略导致了这个问题,目前经尝试,在以下几种情况中 navigator.mediaDevices 可以正常使用
- 地址为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() ->
Dart
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打开摄像头的问题。
Dart
controller.platform.setOnPlatformPermissionRequest(
(request) {
request.grant();
},
);
苹果 wkwebview的内核
如果开始有权限 流程正常,如果开始app没有权限 申请权限后 webview 依旧没有权限 需要退出app重新进才会有权限
原因解决:
之前在ios的info.plist 中声明的是这样的
html
<key>NSCameraUsageDescription</key>
<string></string>
app内申请权限 依旧可以正常使用 并获取到,但是webview不行
html
<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博客
Dart
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
Dart
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<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;
@override
void 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-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<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();
}
@override
Widget 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});
@override
State<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> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: AppTheme.themeColor_black,
appBar: TopAppBar(context, "404"),
body: PlaceholderWidget.emptyPlaceholder(
placeholderType: PlaceholderType.notFound404),
),
);
}
}