feat: 添加媒体权限请求处理;更新 H5ShellPage 以支持音视频权限管理

This commit is contained in:
Booker
2026-05-27 13:31:35 +07:00
parent c27f3cd001
commit e05b75d8df

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
@@ -119,12 +120,21 @@ class _H5ShellPageState extends State<H5ShellPage> {
}
WebViewController _buildController(int lineIndex) {
return WebViewController()
return WebViewController(
onPermissionRequest: (request) {
unawaited(_handleWebViewPermissionRequest(request));
},
)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'OpenIMShell',
onMessageReceived: (message) => _handleShellMessage(lineIndex, message),
)
..addJavaScriptChannel(
'OpenIMFlutterShell',
onMessageReceived: (message) =>
_handleFlutterShellMessage(lineIndex, message),
)
..setBackgroundColor(_shellBackground)
..setNavigationDelegate(
NavigationDelegate(
@@ -211,6 +221,35 @@ class _H5ShellPageState extends State<H5ShellPage> {
}
}
void _handleFlutterShellMessage(int lineIndex, JavaScriptMessage message) {
try {
final decoded = jsonDecode(message.message);
if (decoded is! Map) {
return;
}
switch (decoded['type']) {
case 'requestMediaPermissions':
final requestId = decoded['requestId']?.toString();
if (requestId == null || requestId.isEmpty) {
return;
}
unawaited(
_handleShellMediaPermissionRequest(
lineIndex: lineIndex,
requestId: requestId,
audio: decoded['audio'] == true,
video: decoded['video'] == true,
),
);
case 'openAppSettings':
unawaited(openAppSettings());
}
} catch (_) {
// Ignore malformed shell messages from web content.
}
}
void _scheduleShellCoverFallback(int lineIndex) {
final slot = _lineSlots[lineIndex];
slot.shellCoverFallbackTimer?.cancel();
@@ -309,6 +348,88 @@ class _H5ShellPageState extends State<H5ShellPage> {
return _runJavaScriptSafely(lineIndex, script);
}
Future<void> _handleShellMediaPermissionRequest({
required int lineIndex,
required String requestId,
required bool audio,
required bool video,
}) async {
final result = await _requestNativeMediaPermissions(
audio: audio,
video: video,
);
final payload = jsonEncode({
'requestId': requestId,
'granted': result.granted,
'permanentlyDenied': result.permanentlyDenied,
'restricted': result.restricted,
});
final script = '''
(() => {
try {
window.dispatchEvent(new CustomEvent('openim-shell-media-permission-result', { detail: $payload }));
} catch (_) {}
})();
''';
await _runJavaScriptSafely(lineIndex, script);
}
Future<void> _handleWebViewPermissionRequest(
WebViewPermissionRequest request,
) async {
final requestsOnlySupportedMediaTypes = request.types.every(
(type) =>
type == WebViewPermissionResourceType.camera ||
type == WebViewPermissionResourceType.microphone,
);
if (!requestsOnlySupportedMediaTypes) {
await request.deny();
return;
}
final result = await _requestNativeMediaPermissions(
audio: request.types.contains(WebViewPermissionResourceType.microphone),
video: request.types.contains(WebViewPermissionResourceType.camera),
);
if (result.granted) {
await request.grant();
} else {
await request.deny();
}
}
Future<_NativeMediaPermissionResult> _requestNativeMediaPermissions({
required bool audio,
required bool video,
}) async {
final permissions = <Permission>[
if (audio) Permission.microphone,
if (video) Permission.camera,
];
if (permissions.isEmpty) {
return const _NativeMediaPermissionResult(granted: true);
}
var granted = true;
var permanentlyDenied = false;
var restricted = false;
for (final permission in permissions) {
final status = await permission.request();
granted = granted && status.isGranted;
permanentlyDenied = permanentlyDenied || status.isPermanentlyDenied;
restricted = restricted || status.isRestricted;
}
return _NativeMediaPermissionResult(
granted: granted,
permanentlyDenied: permanentlyDenied,
restricted: restricted,
);
}
void _updateSlotUrl(int lineIndex, String? url) {
if (url == null || lineIndex < 0 || lineIndex >= _lineSlots.length) {
return;
@@ -499,6 +620,18 @@ class _H5ShellPageState extends State<H5ShellPage> {
}
}
class _NativeMediaPermissionResult {
const _NativeMediaPermissionResult({
required this.granted,
this.permanentlyDenied = false,
this.restricted = false,
});
final bool granted;
final bool permanentlyDenied;
final bool restricted;
}
class _H5LineWebViewSlot {
_H5LineWebViewSlot({
required this.line,