diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index d5801c7a6516..eb7b1bc4684f 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 838987714d31..80431ad4cd5b 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 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { registerJavaScriptChannelNames((List) 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 updateJsMode(int mode) { } } + 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 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 000000000000..27e17104277b Binary files /dev/null and b/packages/webview_flutter/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml index 6ec8a1ddd0ea..8657cfde0f8b 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 @@ dev_dependencies: 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 7a290a05bd5b..b6abff94e6b2 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 @@ void main() { await controller.evaluateJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['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 = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + test('Auto media playback', () async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + 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(); + pageLoaded = Completer(); + + // 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 controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + 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(); + + 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 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 c56d5c7dbcc3..87cb0f57377b 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -62,8 +62,12 @@ - (instancetype)initWithFrame:(CGRect)frame [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; } + NSDictionary* 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 @@ - (instancetype)initWithFrame:(CGRect)frame [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf onMethodCall:call result:result]; }]; - NSDictionary* 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)updateJsMode:(NSNumber*)mode { } } +- (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*)request { if (!request) { return false; diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index bfb5b6255421..4e6b8b86cacf 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -183,9 +183,16 @@ class WebSettings { } /// 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 @@ class CreationParams { // to PlatformWebView. final Set 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 ce9988743853..a914e1828b6b 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 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { 'removeJavascriptChannels', javascriptChannelNames.toList()); } - /// Method channel mplementation for [WebViewPlatform.clearCookies]. + /// Method channel implementation for [WebViewPlatform.clearCookies]. static Future clearCookies() { return _cookieManagerChannel .invokeMethod('clearCookies') @@ -135,6 +135,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { '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 3c0175e2f22b..4335ed27ada8 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -72,6 +72,25 @@ typedef NavigationDecision NavigationDelegate(NavigationRequest navigation); /// 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 @@ class WebView extends StatefulWidget { /// 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 @@ class WebView extends StatefulWidget { 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 @@ class WebView extends StatefulWidget { /// 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 createState() => _WebViewState(); } @@ -317,6 +347,7 @@ CreationParams _creationParamsfromWidget(WebView widget) { initialUrl: widget.initialUrl, webSettings: _webSettingsFromWidget(widget), javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, ); } @@ -351,9 +382,10 @@ WebSettings _clearUnchangedWebSettings( } return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - debuggingEnabled: debuggingEnabled); + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + debuggingEnabled: debuggingEnabled, + ); } Set _extractChannelNames(Set channels) { diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index c4940c215e0e..6cde699ea336 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 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter