[webview_flutter] Add an initialAutoMediaPlaybackPolicy setting (#1951)
Controls whether a user action (e.g is touch event) is required in order to start media playback.
diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md
index d5801c7..eb7b1bc 100644
--- a/packages/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.3.11
+
+* Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media
+ playback is restricted.
+
## 0.3.10+5
* Add dependency on `androidx.annotation:annotation:1.0.0`.
diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
index 8389877..80431ad 100644
--- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
+++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java
@@ -51,6 +51,7 @@
registerJavaScriptChannelNames((List<String>) params.get(JS_CHANNEL_NAMES_FIELD));
}
+ updateAutoMediaPlaybackPolicy((Integer) params.get("autoMediaPlaybackPolicy"));
if (params.containsKey("initialUrl")) {
String url = (String) params.get("initialUrl");
webView.loadUrl(url);
@@ -251,6 +252,13 @@
}
}
+ private void updateAutoMediaPlaybackPolicy(int mode) {
+ // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all
+ // other values we require a user gesture.
+ boolean requireUserGesture = mode != 1;
+ webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture);
+ }
+
private void registerJavaScriptChannelNames(List<String> channelNames) {
for (String channelName : channelNames) {
webView.addJavascriptInterface(
diff --git a/packages/webview_flutter/example/assets/sample_audio.ogg b/packages/webview_flutter/example/assets/sample_audio.ogg
new file mode 100644
index 0000000..27e1710
--- /dev/null
+++ b/packages/webview_flutter/example/assets/sample_audio.ogg
Binary files differ
diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml
index 6ec8a1d..8657cfd 100644
--- a/packages/webview_flutter/example/pubspec.yaml
+++ b/packages/webview_flutter/example/pubspec.yaml
@@ -1,7 +1,7 @@
name: webview_flutter_example
description: Demonstrates how to use the webview_flutter plugin.
-version: 1.0.0+1
+version: 1.0.1
environment:
sdk: ">=2.0.0-dev.68.0 <3.0.0"
@@ -20,3 +20,5 @@
flutter:
uses-material-design: true
+ assets:
+ - assets/sample_audio.ogg
diff --git a/packages/webview_flutter/example/test_driver/webview.dart b/packages/webview_flutter/example/test_driver/webview.dart
index 7a290a0..b6abff9 100644
--- a/packages/webview_flutter/example/test_driver/webview.dart
+++ b/packages/webview_flutter/example/test_driver/webview.dart
@@ -3,7 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -132,9 +136,166 @@
await controller.evaluateJavascript('Echo.postMessage("hello");');
expect(messagesReceived, equals(<String>['hello']));
});
+
+ group('Media playback policy', () {
+ String audioTestBase64;
+ setUpAll(() async {
+ final ByteData audioData =
+ await rootBundle.load('assets/sample_audio.ogg');
+ final String base64AudioData =
+ base64Encode(Uint8List.view(audioData.buffer));
+ final String audioTest = '''
+ <!DOCTYPE html><html>
+ <head><title>Audio auto play</title>
+ <script type="text/javascript">
+ function play() {
+ var audio = document.getElementById("audio");
+ audio.play();
+ }
+ function isPaused() {
+ var audio = document.getElementById("audio");
+ return audio.paused;
+ }
+ </script>
+ </head>
+ <body onload="play();">
+ <audio controls id="audio">
+ <source src="data:audio/ogg;charset=utf-8;base64,$base64AudioData">
+ </audio>
+ </body>
+ </html>
+ ''';
+ audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest));
+ });
+
+ test('Auto media playback', () async {
+ Completer<WebViewController> controllerCompleter =
+ Completer<WebViewController>();
+ Completer<void> pageLoaded = Completer<void>();
+
+ await pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: WebView(
+ key: GlobalKey(),
+ initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+ onWebViewCreated: (WebViewController controller) {
+ controllerCompleter.complete(controller);
+ },
+ javascriptMode: JavascriptMode.unrestricted,
+ onPageFinished: (String url) {
+ pageLoaded.complete(null);
+ },
+ initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+ ),
+ ),
+ );
+ WebViewController controller = await controllerCompleter.future;
+ await pageLoaded.future;
+
+ String isPaused = await controller.evaluateJavascript('isPaused();');
+ expect(isPaused, _webviewBool(false));
+
+ controllerCompleter = Completer<WebViewController>();
+ pageLoaded = Completer<void>();
+
+ // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy
+ await pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: WebView(
+ key: GlobalKey(),
+ initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+ onWebViewCreated: (WebViewController controller) {
+ controllerCompleter.complete(controller);
+ },
+ javascriptMode: JavascriptMode.unrestricted,
+ onPageFinished: (String url) {
+ pageLoaded.complete(null);
+ },
+ initialMediaPlaybackPolicy:
+ AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+ ),
+ ),
+ );
+
+ controller = await controllerCompleter.future;
+ await pageLoaded.future;
+
+ isPaused = await controller.evaluateJavascript('isPaused();');
+ expect(isPaused, _webviewBool(true));
+ });
+
+ test('Changes to initialMediaPlaybackPolocy are ignored', () async {
+ final Completer<WebViewController> controllerCompleter =
+ Completer<WebViewController>();
+ Completer<void> pageLoaded = Completer<void>();
+
+ final GlobalKey key = GlobalKey();
+ await pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: WebView(
+ key: key,
+ initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+ onWebViewCreated: (WebViewController controller) {
+ controllerCompleter.complete(controller);
+ },
+ javascriptMode: JavascriptMode.unrestricted,
+ onPageFinished: (String url) {
+ pageLoaded.complete(null);
+ },
+ initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
+ ),
+ ),
+ );
+ final WebViewController controller = await controllerCompleter.future;
+ await pageLoaded.future;
+
+ String isPaused = await controller.evaluateJavascript('isPaused();');
+ expect(isPaused, _webviewBool(false));
+
+ pageLoaded = Completer<void>();
+
+ await pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: WebView(
+ key: key,
+ initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64',
+ onWebViewCreated: (WebViewController controller) {
+ controllerCompleter.complete(controller);
+ },
+ javascriptMode: JavascriptMode.unrestricted,
+ onPageFinished: (String url) {
+ pageLoaded.complete(null);
+ },
+ initialMediaPlaybackPolicy:
+ AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+ ),
+ ),
+ );
+
+ await controller.reload();
+
+ await pageLoaded.future;
+
+ isPaused = await controller.evaluateJavascript('isPaused();');
+ expect(isPaused, _webviewBool(false));
+ });
+ });
}
Future<void> pumpWidget(Widget widget) {
runApp(widget);
return WidgetsBinding.instance.endOfFrame;
}
+
+// JavaScript booleans evaluate to different string values on Android and iOS.
+// This utility method returns the string boolean value of the current platform.
+String _webviewBool(bool value) {
+ if (defaultTargetPlatform == TargetPlatform.iOS) {
+ return value ? '1' : '0';
+ }
+ return value ? 'true' : 'false';
+}
diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m
index c56d5c7..87cb0f5 100644
--- a/packages/webview_flutter/ios/Classes/FlutterWebView.m
+++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m
@@ -62,8 +62,12 @@
[self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController];
}
+ NSDictionary<NSString*, id>* settings = args[@"settings"];
+
WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
configuration.userContentController = userContentController;
+ [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"]
+ inConfiguration:configuration];
_webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];
_navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel];
@@ -72,7 +76,7 @@
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
- NSDictionary<NSString*, id>* settings = args[@"settings"];
+
[self applySettings:settings];
// TODO(amirh): return an error if apply settings failed once it's possible to do so.
// https://github.com/flutter/flutter/issues/36228
@@ -271,6 +275,30 @@
}
}
+- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy
+ inConfiguration:(WKWebViewConfiguration*)configuration {
+ switch ([policy integerValue]) {
+ case 0: // require_user_action_for_all_media_types
+ NSLog(@"requiring user action for all types");
+ if (@available(iOS 10.0, *)) {
+ configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll;
+ } else {
+ configuration.mediaPlaybackRequiresUserAction = true;
+ }
+ break;
+ case 1: // always_allow
+ NSLog(@"allowing auto playback");
+ if (@available(iOS 10.0, *)) {
+ configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone;
+ } else {
+ configuration.mediaPlaybackRequiresUserAction = false;
+ }
+ break;
+ default:
+ NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy);
+ }
+}
+
- (bool)loadRequest:(NSDictionary<NSString*, id>*)request {
if (!request) {
return false;
diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart
index bfb5b62..4e6b8b8 100644
--- a/packages/webview_flutter/lib/platform_interface.dart
+++ b/packages/webview_flutter/lib/platform_interface.dart
@@ -183,9 +183,16 @@
}
/// Configuration to use when creating a new [WebViewPlatformController].
+///
+/// The `autoMediaPlaybackPolicy` parameter must not be null.
class CreationParams {
- CreationParams(
- {this.initialUrl, this.webSettings, this.javascriptChannelNames});
+ CreationParams({
+ this.initialUrl,
+ this.webSettings,
+ this.javascriptChannelNames,
+ this.autoMediaPlaybackPolicy =
+ AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+ }) : assert(autoMediaPlaybackPolicy != null);
/// The initialUrl to load in the webview.
///
@@ -210,6 +217,9 @@
// to PlatformWebView.
final Set<String> javascriptChannelNames;
+ /// Which restrictions apply on automatic media playback.
+ final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy;
+
@override
String toString() {
return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames)';
diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart
index ce99887..a914e18 100644
--- a/packages/webview_flutter/lib/src/webview_method_channel.dart
+++ b/packages/webview_flutter/lib/src/webview_method_channel.dart
@@ -103,7 +103,7 @@
'removeJavascriptChannels', javascriptChannelNames.toList());
}
- /// Method channel mplementation for [WebViewPlatform.clearCookies].
+ /// Method channel implementation for [WebViewPlatform.clearCookies].
static Future<bool> clearCookies() {
return _cookieManagerChannel
.invokeMethod<bool>('clearCookies')
@@ -135,6 +135,7 @@
'initialUrl': creationParams.initialUrl,
'settings': _webSettingsToMap(creationParams.webSettings),
'javascriptChannelNames': creationParams.javascriptChannelNames.toList(),
+ 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index,
};
}
}
diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart
index 3c0175e..4335ed2 100644
--- a/packages/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/lib/webview_flutter.dart
@@ -72,6 +72,25 @@
/// Signature for when a [WebView] has finished loading a page.
typedef void PageFinishedCallback(String url);
+/// Specifies possible restrictions on automatic media playback.
+///
+/// This is typically used in [WebView.initialMediaPlaybackPolicy].
+// The method channel implementation is marshalling this enum to the value's index, so the order
+// is important.
+enum AutoMediaPlaybackPolicy {
+ /// Starting any kind of media playback requires a user action.
+ ///
+ /// For example: JavaScript code cannot start playing media unless the code was executed
+ /// as a result of a user action (like a touch event).
+ require_user_action_for_all_media_types,
+
+ /// Starting any kind of media playback is always allowed.
+ ///
+ /// For example: JavaScript code that's triggered when the page is loaded can start playing
+ /// video or audio.
+ always_allow,
+}
+
final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$');
/// A named channel for receiving messaged from JavaScript code running inside a web view.
@@ -110,7 +129,7 @@
/// The web view can be controlled using a `WebViewController` that is passed to the
/// `onWebViewCreated` callback once the web view is created.
///
- /// The `javascriptMode` parameter must not be null.
+ /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null.
const WebView({
Key key,
this.onWebViewCreated,
@@ -121,7 +140,10 @@
this.gestureRecognizers,
this.onPageFinished,
this.debuggingEnabled = false,
+ this.initialMediaPlaybackPolicy =
+ AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
}) : assert(javascriptMode != null),
+ assert(initialMediaPlaybackPolicy != null),
super(key: key);
static WebViewPlatform _platform;
@@ -255,6 +277,14 @@
/// By default `debuggingEnabled` is false.
final bool debuggingEnabled;
+ /// Which restrictions apply on automatic media playback.
+ ///
+ /// This initial value is applied to the platform's webview upon creation. Any following
+ /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved).
+ ///
+ /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types].
+ final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy;
+
@override
State<StatefulWidget> createState() => _WebViewState();
}
@@ -317,6 +347,7 @@
initialUrl: widget.initialUrl,
webSettings: _webSettingsFromWidget(widget),
javascriptChannelNames: _extractChannelNames(widget.javascriptChannels),
+ autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy,
);
}
@@ -351,9 +382,10 @@
}
return WebSettings(
- javascriptMode: javascriptMode,
- hasNavigationDelegate: hasNavigationDelegate,
- debuggingEnabled: debuggingEnabled);
+ javascriptMode: javascriptMode,
+ hasNavigationDelegate: hasNavigationDelegate,
+ debuggingEnabled: debuggingEnabled,
+ );
}
Set<String> _extractChannelNames(Set<JavascriptChannel> channels) {
diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml
index c4940c2..6cde699 100644
--- a/packages/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/pubspec.yaml
@@ -1,6 +1,6 @@
name: webview_flutter
description: A Flutter plugin that provides a WebView widget on Android and iOS.
-version: 0.3.10+5
+version: 0.3.11
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter