diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 58e6f6466ed3..9be0a8cc946b 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Adds web options to customize the control list and context menu display. + ## 2.0.18 * Migrates to `dart:ui_web` APIs. diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart index 28046f42e9a8..d5ede903cda4 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_test.dart @@ -213,5 +213,199 @@ void main() { expect(events[0].duration, equals(jsCompatibleTimeUnset)); }); }); + + group('VideoPlayerWebOptions', () { + late VideoPlayer player; + + setUp(() { + video = html.VideoElement(); + player = VideoPlayer(videoElement: video)..initialize(); + }); + + group('VideoPlayerWebOptionsControls', () { + testWidgets('when disabled expect no controls', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + // ignore: avoid_redundant_argument_values + controls: VideoPlayerWebOptionsControls.disabled(), + ), + ); + + expect(video.controls, isFalse); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, isZero); + }); + + group('when enabled', () { + testWidgets('expect controls', (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled(), + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, isZero); + expect(video.controlsList?.contains('nodownload'), isFalse); + expect(video.controlsList?.contains('nofullscreen'), isFalse); + expect(video.controlsList?.contains('noplaybackrate'), isFalse); + expect(video.getAttribute('disablePictureInPicture'), isNull); + }); + + testWidgets('and no download expect correct controls', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled( + allowDownload: false, + ), + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 1); + expect(video.controlsList?.contains('nodownload'), isTrue); + expect(video.controlsList?.contains('nofullscreen'), isFalse); + expect(video.controlsList?.contains('noplaybackrate'), isFalse); + expect(video.getAttribute('disablePictureInPicture'), isNull); + }); + + testWidgets('and no fullscreen expect correct controls', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled( + allowFullscreen: false, + ), + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 1); + expect(video.controlsList?.contains('nodownload'), isFalse); + expect(video.controlsList?.contains('nofullscreen'), isTrue); + expect(video.controlsList?.contains('noplaybackrate'), isFalse); + expect(video.getAttribute('disablePictureInPicture'), isNull); + }); + + testWidgets('and no playback rate expect correct controls', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled( + allowPlaybackRate: false, + ), + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 1); + expect(video.controlsList?.contains('nodownload'), isFalse); + expect(video.controlsList?.contains('nofullscreen'), isFalse); + expect(video.controlsList?.contains('noplaybackrate'), isTrue); + expect(video.getAttribute('disablePictureInPicture'), isNull); + }); + + testWidgets('and no picture in picture expect correct controls', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled( + allowPictureInPicture: false, + ), + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 0); + expect(video.controlsList?.contains('nodownload'), isFalse); + expect(video.controlsList?.contains('nofullscreen'), isFalse); + expect(video.controlsList?.contains('noplaybackrate'), isFalse); + expect(video.getAttribute('disablePictureInPicture'), 'true'); + }); + }); + }); + + group('allowRemotePlayback', () { + testWidgets('when enabled expect no attribute', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + // ignore: avoid_redundant_argument_values + allowRemotePlayback: true, + ), + ); + + expect(video.getAttribute('disableRemotePlayback'), isNull); + }); + + testWidgets('when disabled expect attribute', + (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + allowRemotePlayback: false, + ), + ); + + expect(video.getAttribute('disableRemotePlayback'), 'true'); + }); + }); + + group('when called first time', () { + testWidgets('expect correct options', (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + controls: VideoPlayerWebOptionsControls.enabled( + allowDownload: false, + allowFullscreen: false, + allowPlaybackRate: false, + allowPictureInPicture: false, + ), + allowContextMenu: false, + allowRemotePlayback: false, + ), + ); + + expect(video.controls, isTrue); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 3); + expect(video.controlsList?.contains('nodownload'), isTrue); + expect(video.controlsList?.contains('nofullscreen'), isTrue); + expect(video.controlsList?.contains('noplaybackrate'), isTrue); + expect(video.getAttribute('disablePictureInPicture'), 'true'); + expect(video.getAttribute('disableRemotePlayback'), 'true'); + }); + + group('when called once more', () { + testWidgets('expect correct options', (WidgetTester tester) async { + await player.setOptions( + const VideoPlayerWebOptions( + // ignore: avoid_redundant_argument_values + controls: VideoPlayerWebOptionsControls.disabled(), + // ignore: avoid_redundant_argument_values + allowContextMenu: true, + // ignore: avoid_redundant_argument_values + allowRemotePlayback: true, + ), + ); + + expect(video.controls, isFalse); + expect(video.controlsList, isNotNull); + expect(video.controlsList?.length, 0); + expect(video.controlsList?.contains('nodownload'), isFalse); + expect(video.controlsList?.contains('nofullscreen'), isFalse); + expect(video.controlsList?.contains('noplaybackrate'), isFalse); + expect(video.getAttribute('disablePictureInPicture'), isNull); + expect(video.getAttribute('disableRemotePlayback'), isNull); + }); + }); + }); + }); }); } diff --git a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart index 7d7422393097..7c8f1c9acefc 100644 --- a/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -239,5 +239,15 @@ void main() { VideoEventType.bufferingEnd, ])); }); + + testWidgets('can set web options', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.setWebOptions( + await textureId, + const VideoPlayerWebOptions(), + ), + completes, + ); + }); }); } diff --git a/packages/video_player/video_player_web/lib/src/video_player.dart b/packages/video_player/video_player_web/lib/src/video_player.dart index bc0021dee34b..3d047be76581 100644 --- a/packages/video_player/video_player_web/lib/src/video_player.dart +++ b/packages/video_player/video_player_web/lib/src/video_player.dart @@ -45,6 +45,7 @@ class VideoPlayer { final StreamController _eventController; final html.VideoElement _videoElement; + void Function(html.Event)? _onContextMenu; bool _isInitialized = false; bool _isBuffering = false; @@ -202,9 +203,51 @@ class VideoPlayer { return Duration(milliseconds: (_videoElement.currentTime * 1000).round()); } + /// Sets options + Future setOptions(VideoPlayerWebOptions options) async { + // In case this method is called multiple times, reset options. + _resetOptions(); + + if (options.controls.enabled) { + _videoElement.controls = true; + final String controlsList = options.controls.controlsList; + if (controlsList.isNotEmpty) { + _videoElement.setAttribute('controlsList', controlsList); + } + + if (!options.controls.allowPictureInPicture) { + _videoElement.setAttribute('disablePictureInPicture', true); + } + } + + if (!options.allowContextMenu) { + _onContextMenu = (html.Event event) => event.preventDefault(); + _videoElement.addEventListener('contextmenu', _onContextMenu); + } + + if (!options.allowRemotePlayback) { + _videoElement.setAttribute('disableRemotePlayback', true); + } + } + + void _resetOptions() { + _videoElement.controls = false; + _videoElement.removeAttribute('controlsList'); + _videoElement.removeAttribute('disablePictureInPicture'); + if (_onContextMenu != null) { + _videoElement.removeEventListener('contextmenu', _onContextMenu); + _onContextMenu = null; + } + _videoElement.removeAttribute('disableRemotePlayback'); + } + /// Disposes of the current [html.VideoElement]. void dispose() { _videoElement.removeAttribute('src'); + if (_onContextMenu != null) { + _videoElement.removeEventListener('contextmenu', _onContextMenu); + _onContextMenu = null; + } _videoElement.load(); } diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index caa585b9198d..77b3cca2804b 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -132,6 +132,11 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { return _player(textureId).events; } + @override + Future setWebOptions(int textureId, VideoPlayerWebOptions options) { + return _player(textureId).setOptions(options); + } + // Retrieves a [VideoPlayer] by its internal `id`. // It must have been created earlier from the [create] method. VideoPlayer _player(int id) { diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index c0afc0598d2f..4fc976afb5af 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.18 +version: 2.1.0 environment: sdk: ">=3.1.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - video_player_platform_interface: ">=6.1.0 <7.0.0" + video_player_platform_interface: ^6.2.0 dev_dependencies: flutter_test: