feat: 添加媒体权限请求桥接脚本,优化 WebView 媒体权限处理逻辑
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
<!-- 相机和麦克风权限 -->
|
<!-- 相机和麦克风权限 -->
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
<!-- 位置权限(如果需要) -->
|
<!-- 位置权限(如果需要) -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class AppConfig {
|
|||||||
}) {
|
}) {
|
||||||
final uri = Uri.parse(url);
|
final uri = Uri.parse(url);
|
||||||
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
||||||
..['flutter_shell'] = '1';
|
..['flutter_shell'] = '1'
|
||||||
|
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
|
|
||||||
final trimmedName = (appName ?? AppConfig.appName).trim();
|
final trimmedName = (appName ?? AppConfig.appName).trim();
|
||||||
if (trimmedName.isNotEmpty) {
|
if (trimmedName.isNotEmpty) {
|
||||||
|
|||||||
159
lib/main.dart
159
lib/main.dart
@@ -17,6 +17,100 @@ const _shellSubText = Color(0xFF8E9AB0);
|
|||||||
const _resumeCoverDuration = Duration(milliseconds: 700);
|
const _resumeCoverDuration = Duration(milliseconds: 700);
|
||||||
const _shellBrandingChannel =
|
const _shellBrandingChannel =
|
||||||
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
|
||||||
|
const _shellMediaPermissionBridgeScript = r'''
|
||||||
|
(() => {
|
||||||
|
if (window.__openimShellMediaBridgeInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = window.OpenIMFlutterShell;
|
||||||
|
const mediaDevices = navigator.mediaDevices;
|
||||||
|
if (!channel || typeof channel.postMessage !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaDevices || typeof mediaDevices.getUserMedia !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalGetUserMedia = mediaDevices.getUserMedia.bind(mediaDevices);
|
||||||
|
const resultEventName = 'openim-shell-media-permission-result';
|
||||||
|
|
||||||
|
const createRequestId = () => {
|
||||||
|
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
|
||||||
|
return window.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPermissionError = () => {
|
||||||
|
try {
|
||||||
|
return new DOMException('Permission denied', 'NotAllowedError');
|
||||||
|
} catch (_) {
|
||||||
|
const error = new Error('Permission denied');
|
||||||
|
error.name = 'NotAllowedError';
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestNativePermissions = (constraints) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const requestId = createRequestId();
|
||||||
|
let timer;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (timer) {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
window.removeEventListener(resultEventName, listener);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const detail = event.detail;
|
||||||
|
if (!detail || detail.requestId !== requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
resolve(detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
resolve(undefined);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
window.addEventListener(resultEventName, listener);
|
||||||
|
channel.postMessage(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'requestMediaPermissions',
|
||||||
|
requestId,
|
||||||
|
audio: Boolean(constraints && constraints.audio),
|
||||||
|
video: Boolean(constraints && constraints.video),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrappedGetUserMedia = async (constraints) => {
|
||||||
|
const result = await requestNativePermissions(constraints || {});
|
||||||
|
if (result && result.granted) {
|
||||||
|
return originalGetUserMedia(constraints);
|
||||||
|
}
|
||||||
|
throw createPermissionError();
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object.defineProperty(mediaDevices, 'getUserMedia', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: wrappedGetUserMedia,
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
mediaDevices.getUserMedia = wrappedGetUserMedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__openimShellMediaBridgeInstalled = true;
|
||||||
|
window.dispatchEvent(new CustomEvent('openim-shell-media-bridge-ready'));
|
||||||
|
})();
|
||||||
|
''';
|
||||||
const _stopWebMediaScript = r'''
|
const _stopWebMediaScript = r'''
|
||||||
(() => {
|
(() => {
|
||||||
try {
|
try {
|
||||||
@@ -192,6 +286,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
onPageFinished: (_) {
|
onPageFinished: (_) {
|
||||||
unawaited(_syncShellBranding());
|
unawaited(_syncShellBranding());
|
||||||
|
unawaited(_injectShellMediaPermissionBridge());
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_progress = 100;
|
_progress = 100;
|
||||||
@@ -275,12 +370,10 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
permissions.add(Permission.camera);
|
permissions.add(Permission.camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
final statuses = permissions.isEmpty
|
final statusesByPermission = await _requestPermissions(permissions);
|
||||||
? <PermissionStatus>[]
|
final statuses = statusesByPermission.values;
|
||||||
: await Future.wait(
|
|
||||||
permissions.map((permission) => permission.request()));
|
|
||||||
final granted = statuses.every(
|
final granted = statuses.every(
|
||||||
(status) => status.isGranted || status.isLimited,
|
_isPermissionAllowed,
|
||||||
);
|
);
|
||||||
final permanentlyDenied =
|
final permanentlyDenied =
|
||||||
statuses.any((status) => status.isPermanentlyDenied);
|
statuses.any((status) => status.isPermanentlyDenied);
|
||||||
@@ -337,6 +430,10 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
return _runJavaScriptSafely(script);
|
return _runJavaScriptSafely(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _injectShellMediaPermissionBridge() {
|
||||||
|
return _runJavaScriptSafely(_shellMediaPermissionBridgeScript);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _stopWebMedia() {
|
Future<void> _stopWebMedia() {
|
||||||
return _runJavaScriptSafely(_stopWebMediaScript);
|
return _runJavaScriptSafely(_stopWebMediaScript);
|
||||||
}
|
}
|
||||||
@@ -368,7 +465,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
Future<void> _handleWebViewPermissionRequest(
|
Future<void> _handleWebViewPermissionRequest(
|
||||||
WebViewPermissionRequest request,
|
WebViewPermissionRequest request,
|
||||||
) async {
|
) async {
|
||||||
final permissions = <Permission>[];
|
final permissions = <Permission>{};
|
||||||
if (request.types.contains(WebViewPermissionResourceType.camera)) {
|
if (request.types.contains(WebViewPermissionResourceType.camera)) {
|
||||||
permissions.add(Permission.camera);
|
permissions.add(Permission.camera);
|
||||||
}
|
}
|
||||||
@@ -376,9 +473,17 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
permissions.add(Permission.microphone);
|
permissions.add(Permission.microphone);
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowed = permissions.isEmpty ||
|
debugPrint(
|
||||||
await Future.wait(permissions.map(_requestPermission))
|
'[H5Shell] WebView media permission request: '
|
||||||
.then((results) => results.every((allowed) => allowed));
|
'${request.types.map((type) => type.name).join(', ')}',
|
||||||
|
);
|
||||||
|
|
||||||
|
final statusesByPermission = await _requestPermissions(permissions);
|
||||||
|
final allowed = statusesByPermission.values.every(_isPermissionAllowed);
|
||||||
|
debugPrint(
|
||||||
|
'[H5Shell] WebView media permission result: '
|
||||||
|
'${statusesByPermission.map((permission, status) => MapEntry(permission.toString(), status.toString()))}',
|
||||||
|
);
|
||||||
|
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
await request.grant();
|
await request.grant();
|
||||||
@@ -387,11 +492,43 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _requestPermission(Permission permission) async {
|
bool _isPermissionAllowed(PermissionStatus status) {
|
||||||
final status = await permission.request();
|
|
||||||
return status.isGranted || status.isLimited;
|
return status.isGranted || status.isLimited;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<Permission, PermissionStatus>> _requestPermissions(
|
||||||
|
Set<Permission> permissions,
|
||||||
|
) async {
|
||||||
|
if (permissions.isEmpty) {
|
||||||
|
return const {};
|
||||||
|
}
|
||||||
|
|
||||||
|
final pending = <Permission>[];
|
||||||
|
final statuses = <Permission, PermissionStatus>{};
|
||||||
|
|
||||||
|
for (final permission in permissions) {
|
||||||
|
final status = await permission.status;
|
||||||
|
if (_isPermissionAllowed(status) ||
|
||||||
|
status.isPermanentlyDenied ||
|
||||||
|
status.isRestricted) {
|
||||||
|
statuses[permission] = status;
|
||||||
|
} else {
|
||||||
|
pending.add(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.isNotEmpty) {
|
||||||
|
statuses.addAll(await pending.request());
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _requestPermission(Permission permission) async {
|
||||||
|
final statuses = await _requestPermissions({permission});
|
||||||
|
return statuses.values.every(_isPermissionAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _handleBackNavigation() async {
|
Future<void> _handleBackNavigation() async {
|
||||||
await _stopWebMedia();
|
await _stopWebMedia();
|
||||||
if (await _controller.canGoBack()) {
|
if (await _controller.canGoBack()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user