diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md index e6b95937a..8fb14580e 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 2.4.0 +* Update video_player to 2.4.2. +* Update video_player_platform_interface to 5.1.2. +* Update the example app and integration_test. +* Migrate to new analysis options. +* Remove obsolete dependency on pedantic. * Code cleanups. ## 2.3.2 diff --git a/packages/video_player/README.md b/packages/video_player/README.md index 1106b48b6..73ee3d9a0 100644 --- a/packages/video_player/README.md +++ b/packages/video_player/README.md @@ -28,8 +28,8 @@ This package is not an _endorsed_ implementation of `video_player`. Therefore, y ```yaml dependencies: - video_player: ^2.2.6 - video_player_tizen: ^2.3.2 + video_player: ^2.4.2 + video_player_tizen: ^2.4.0 ``` Then you can import `video_player` in your Dart code: @@ -42,7 +42,11 @@ For detailed usage, see https://pub.dev/packages/video_player#example. ## Limitations -The `httpHeaders` option of `VideoPlayerController.network` and the `mixWithOthers` option of `VideoPlayerOptions` will be silently ignored in Tizen platform. +The following options are not supported on Tizen. + +- The `httpHeaders` option of `VideoPlayerController.network` +- `VideoPlayerOptions.allowBackgroundPlayback` +- `VideoPlayerOptions.mixWithOthers` This plugin has some limitations on TV devices. diff --git a/packages/video_player/analysis_options.yaml b/packages/video_player/analysis_options.yaml deleted file mode 100644 index cda4f6e15..000000000 --- a/packages/video_player/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../analysis_options_legacy.yaml diff --git a/packages/video_player/example/assets/Audio.mp3 b/packages/video_player/example/assets/Audio.mp3 new file mode 100644 index 000000000..355eb9b2e Binary files /dev/null and b/packages/video_player/example/assets/Audio.mp3 differ diff --git a/packages/video_player/example/assets/Butterfly-209.webm b/packages/video_player/example/assets/Butterfly-209.webm new file mode 100644 index 000000000..991bdc710 Binary files /dev/null and b/packages/video_player/example/assets/Butterfly-209.webm differ diff --git a/packages/video_player/example/integration_test/video_player_test.dart b/packages/video_player/example/integration_test/video_player_test.dart index fd147da92..f1e49ade7 100644 --- a/packages/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/example/integration_test/video_player_test.dart @@ -3,15 +3,36 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:integration_test/integration_test.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:video_player/video_player.dart'; const Duration _playDuration = Duration(seconds: 1); +// Use WebM for web to allow CI to use Chromium. +const String _videoAssetKey = + kIsWeb ? 'assets/Butterfly-209.webm' : 'assets/Butterfly-209.mp4'; + +// Returns the URL to load an asset from this example app as a network source. +// +// TODO(stuartmorgan): Convert this to a local `HttpServer` that vends the +// assets directly, https://github.com/flutter/flutter/issues/95420 +String getUrlForAssetAsNetworkSource(String assetKey) { + return 'https://github.com/flutter/plugins/blob/' + // This hash can be rolled forward to pick up newly-added assets. + 'cb381ced070d356799dddf24aca38ce0579d3d7b' + '/packages/video_player/video_player/example/' + '$assetKey' + '?raw=true'; +} + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); late VideoPlayerController _controller; @@ -19,7 +40,7 @@ void main() { group('asset videos', () { setUp(() { - _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + _controller = VideoPlayerController.asset(_videoAssetKey); }); testWidgets('can be initialized', (WidgetTester tester) async { @@ -28,73 +49,27 @@ void main() { expect(_controller.value.isInitialized, true); expect(_controller.value.position, const Duration(seconds: 0)); expect(_controller.value.isPlaying, false); + // The WebM version has a slightly different duration than the MP4. expect(_controller.value.duration, - const Duration(seconds: 7, milliseconds: 540)); + const Duration(seconds: 7, milliseconds: kIsWeb ? 544 : 540)); }); - testWidgets( - 'reports buffering status', - (WidgetTester tester) async { - VideoPlayerController networkController = VideoPlayerController.network( - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', - ); - await networkController.initialize(); - // Mute to allow playing without DOM interaction on Web. - // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes - await networkController.setVolume(0); - final Completer started = Completer(); - final Completer ended = Completer(); - bool startedBuffering = false; - bool endedBuffering = false; - networkController.addListener(() { - if (networkController.value.isBuffering && !startedBuffering) { - startedBuffering = true; - started.complete(); - } - if (startedBuffering && - !networkController.value.isBuffering && - !endedBuffering) { - endedBuffering = true; - ended.complete(); - } - }); - - await networkController.play(); - await networkController.seekTo(const Duration(seconds: 5)); - await tester.pumpAndSettle(_playDuration); - await networkController.pause(); - - expect(networkController.value.isPlaying, false); - expect(networkController.value.position, - (Duration position) => position > const Duration(seconds: 0)); - - await started; - expect(startedBuffering, true); - - await ended; - expect(endedBuffering, true); - }, - skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), - ); - testWidgets( 'live stream duration != 0', (WidgetTester tester) async { - VideoPlayerController networkController = VideoPlayerController.network( - 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + final VideoPlayerController networkController = + VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8', ); await networkController.initialize(); expect(networkController.value.isInitialized, true); // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- - if (defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) { - expect(networkController.value.duration, - (Duration duration) => duration != Duration.zero); - } + expect(networkController.value.duration, + (Duration duration) => duration != Duration.zero); }, - skip: (kIsWeb), + skip: kIsWeb, ); testWidgets( @@ -153,7 +128,7 @@ void main() { // Mute to allow playing without DOM interaction on Web. // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes await _controller.setVolume(0); - Duration timeBeforeEnd = + final Duration timeBeforeEnd = _controller.value.duration - const Duration(milliseconds: 500); await _controller.seekTo(timeBeforeEnd); await _controller.play(); @@ -207,7 +182,7 @@ void main() { child: FutureBuilder( future: started(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { + if (snapshot.data ?? false) { return AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), @@ -223,6 +198,141 @@ void main() { await tester.pumpAndSettle(); expect(_controller.value.isPlaying, true); - }, skip: kIsWeb); // Web does not support local assets. + }, + skip: kIsWeb || // Web does not support local assets. + // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 + defaultTargetPlatform == TargetPlatform.iOS); + }); + + group('file-based videos', () { + setUp(() async { + // Load the data from the asset. + final String tempDir = (await getTemporaryDirectory()).path; + final ByteData bytes = await rootBundle.load(_videoAssetKey); + + // Write it to a file to use as a source. + final String filename = _videoAssetKey.split('/').last; + final File file = File('$tempDir/$filename'); + await file.writeAsBytes(bytes.buffer.asInt8List()); + + _controller = VideoPlayerController.file(file); + }); + + testWidgets('test video player using static file() method as constructor', + (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + expect(_controller.value.isPlaying, true); + + await _controller.pause(); + expect(_controller.value.isPlaying, false); + }, skip: kIsWeb); + }); + + group('network videos', () { + setUp(() { + _controller = VideoPlayerController.network( + getUrlForAssetAsNetworkSource(_videoAssetKey)); + }); + + testWidgets( + 'reports buffering status', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + final Completer started = Completer(); + final Completer ended = Completer(); + _controller.addListener(() { + if (!started.isCompleted && _controller.value.isBuffering) { + started.complete(); + } + if (started.isCompleted && + !_controller.value.isBuffering && + !ended.isCompleted) { + ended.complete(); + } + }); + + await _controller.play(); + await _controller.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await expectLater(started.future, completes); + await expectLater(ended.future, completes); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); + }); + + // Audio playback is tested to prevent accidental regression, + // but could be removed in the future. + group('asset audios', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Audio.mp3'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.isInitialized, true); + expect(_controller.value.position, const Duration(seconds: 0)); + expect(_controller.value.isPlaying, false); + // Due to the duration calculation accurancy between platforms, + // the milliseconds on Web will be a slightly different from natives. + // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. + expect( + _controller.value.duration, + const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41), + ); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, true); + expect( + _controller.value.position, + (Duration position) => position > const Duration(milliseconds: 0), + ); + }); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + final Duration pausedPosition = _controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, pausedPosition); + }); }); } diff --git a/packages/video_player/example/lib/main.dart b/packages/video_player/example/lib/main.dart index 353deabda..1f029076f 100644 --- a/packages/video_player/example/lib/main.dart +++ b/packages/video_player/example/lib/main.dart @@ -45,10 +45,10 @@ class _App extends StatelessWidget { tabs: [ Tab( icon: Icon(Icons.cloud), - text: "Remote", + text: 'Remote', ), - Tab(icon: Icon(Icons.insert_drive_file), text: "Asset"), - Tab(icon: Icon(Icons.list), text: "List example"), + Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), + Tab(icon: Icon(Icons.list), text: 'List example'), ], ), ), @@ -69,20 +69,20 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { Widget build(BuildContext context) { return ListView( children: [ - _ExampleCard(title: "Item a"), - _ExampleCard(title: "Item b"), - _ExampleCard(title: "Item c"), - _ExampleCard(title: "Item d"), - _ExampleCard(title: "Item e"), - _ExampleCard(title: "Item f"), - _ExampleCard(title: "Item g"), + const _ExampleCard(title: 'Item a'), + const _ExampleCard(title: 'Item b'), + const _ExampleCard(title: 'Item c'), + const _ExampleCard(title: 'Item d'), + const _ExampleCard(title: 'Item e'), + const _ExampleCard(title: 'Item f'), + const _ExampleCard(title: 'Item g'), Card( child: Column(children: [ Column( children: [ const ListTile( leading: Icon(Icons.cake), - title: Text("Video video"), + title: Text('Video video'), ), Stack( alignment: FractionalOffset.bottomRight + @@ -94,11 +94,11 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { ], ), ])), - _ExampleCard(title: "Item h"), - _ExampleCard(title: "Item i"), - _ExampleCard(title: "Item j"), - _ExampleCard(title: "Item k"), - _ExampleCard(title: "Item l"), + const _ExampleCard(title: 'Item h'), + const _ExampleCard(title: 'Item i'), + const _ExampleCard(title: 'Item j'), + const _ExampleCard(title: 'Item k'), + const _ExampleCard(title: 'Item l'), ], ); } @@ -267,7 +267,18 @@ class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({Key? key, required this.controller}) : super(key: key); - static const _examplePlaybackRates = [ + static const List _exampleCaptionOffsets = [ + Duration(seconds: -10), + Duration(seconds: -3), + Duration(seconds: -1, milliseconds: -500), + Duration(milliseconds: -250), + Duration(milliseconds: 0), + Duration(milliseconds: 250), + Duration(seconds: 1, milliseconds: 500), + Duration(seconds: 3), + Duration(seconds: 10), + ]; + static const List _examplePlaybackRates = [ 0.25, 0.5, 1.0, @@ -285,13 +296,13 @@ class _ControlsOverlay extends StatelessWidget { return Stack( children: [ AnimatedSwitcher( - duration: Duration(milliseconds: 50), - reverseDuration: Duration(milliseconds: 200), + duration: const Duration(milliseconds: 50), + reverseDuration: const Duration(milliseconds: 200), child: controller.value.isPlaying - ? SizedBox.shrink() + ? const SizedBox.shrink() : Container( color: Colors.black26, - child: Center( + child: const Center( child: Icon( Icons.play_arrow, color: Colors.white, @@ -306,18 +317,47 @@ class _ControlsOverlay extends StatelessWidget { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), + Align( + alignment: Alignment.topLeft, + child: PopupMenuButton( + initialValue: controller.value.captionOffset, + tooltip: 'Caption Offset', + onSelected: (Duration delay) { + controller.setCaptionOffset(delay); + }, + itemBuilder: (BuildContext context) { + return >[ + for (final Duration offsetDuration in _exampleCaptionOffsets) + PopupMenuItem( + value: offsetDuration, + child: Text('${offsetDuration.inMilliseconds}ms'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.captionOffset.inMilliseconds}ms'), + ), + ), + ), Align( alignment: Alignment.topRight, child: PopupMenuButton( initialValue: controller.value.playbackSpeed, tooltip: 'Playback speed', - onSelected: (speed) { + onSelected: (double speed) { controller.setPlaybackSpeed(speed); }, - itemBuilder: (context) { - return [ - for (final speed in _examplePlaybackRates) - PopupMenuItem( + itemBuilder: (BuildContext context) { + return >[ + for (final double speed in _examplePlaybackRates) + PopupMenuItem( value: speed, child: Text('${speed}x'), ) @@ -383,7 +423,7 @@ class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> { child: FutureBuilder( future: started(), builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.data == true) { + if (snapshot.data ?? false) { return AspectRatio( aspectRatio: _videoPlayerController.value.aspectRatio, child: VideoPlayer(_videoPlayerController), diff --git a/packages/video_player/example/pubspec.yaml b/packages/video_player/example/pubspec.yaml index 64cea38db..eafe9ff28 100644 --- a/packages/video_player/example/pubspec.yaml +++ b/packages/video_player/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" dependencies: flutter: sdk: flutter - video_player: ^2.2.6 + video_player: ^2.4.2 video_player_tizen: path: ../ @@ -18,15 +18,19 @@ dev_dependencies: sdk: flutter integration_test_tizen: path: ../../integration_test/ - pedantic: ^1.10.0 + path_provider: ^2.0.6 + path_provider_tizen: + path: ../../path_provider/ test: any flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 + - assets/Butterfly-209.webm - assets/bumble_bee_captions.srt - assets/bumble_bee_captions.vtt + - assets/Audio.mp3 uses-material-design: true environment: diff --git a/packages/video_player/example/test_driver/video_player_test.dart b/packages/video_player/example/test_driver/video_player_test.dart index 1d5ac79c7..5fbed804d 100644 --- a/packages/video_player/example/test_driver/video_player_test.dart +++ b/packages/video_player/example/test_driver/video_player_test.dart @@ -12,8 +12,8 @@ Future main() async { await driver.close(); }); - //TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. - //TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed + // TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. + // TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed test('Push a page contains video and pop back, do not crash.', () async { final SerializableFinder pushTab = find.byValueKey('push_tab'); await driver.waitFor(pushTab); diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 3137d2d54..33d1bbeab 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Tizen. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player -version: 2.3.2 +version: 2.4.0 flutter: plugin: @@ -15,10 +15,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^4.2.0 - -dev_dependencies: - pedantic: ^1.10.0 + video_player_platform_interface: ^5.1.2 environment: flutter: ">=2.0.0" diff --git a/packages/video_player/tizen/src/video_player.cc b/packages/video_player/tizen/src/video_player.cc index 4eb9f26c8..8ce74e03c 100644 --- a/packages/video_player/tizen/src/video_player.cc +++ b/packages/video_player/tizen/src/video_player.cc @@ -138,7 +138,7 @@ VideoPlayer::VideoPlayer(flutter::PluginRegistrar *plugin_registrar, get_error_message(ret)); } - ret = player_set_error_cb(player_, OnErrorOccurred, this); + ret = player_set_error_cb(player_, OnError, this); if (ret != PLAYER_ERROR_NONE) { player_destroy(player_); throw VideoPlayerError("player_set_error_cb failed", @@ -291,14 +291,12 @@ void VideoPlayer::SetUpEventChannel(flutter::BinaryMessenger *messenger) { [&](const flutter::EncodableValue *arguments, std::unique_ptr> &&events) -> std::unique_ptr> { - LOG_DEBUG("[VideoPlayer] listening on %s", name.c_str()); event_sink_ = std::move(events); Initialize(); return nullptr; }, [&](const flutter::EncodableValue *arguments) -> std::unique_ptr> { - LOG_DEBUG("[VideoPlayer] cancel listening"); event_sink_ = nullptr; return nullptr; }); @@ -414,7 +412,7 @@ void VideoPlayer::OnInterrupted(player_interrupted_code_e code, void *data) { } } -void VideoPlayer::OnErrorOccurred(int code, void *data) { +void VideoPlayer::OnError(int code, void *data) { auto *player = reinterpret_cast(data); LOG_DEBUG("[VideoPlayer] error code: %d", code); diff --git a/packages/video_player/tizen/src/video_player.h b/packages/video_player/tizen/src/video_player.h index 464e993b3..8aaa3925f 100644 --- a/packages/video_player/tizen/src/video_player.h +++ b/packages/video_player/tizen/src/video_player.h @@ -50,7 +50,7 @@ class VideoPlayer { static void OnSeekCompleted(void *data); static void OnPlayCompleted(void *data); static void OnInterrupted(player_interrupted_code_e code, void *data); - static void OnErrorOccurred(int code, void *data); + static void OnError(int code, void *data); static void OnVideoFrameDecoded(media_packet_h packet, void *data); bool is_initialized_;