diff --git a/CHANGELOG.md b/CHANGELOG.md index c44004ebb..709cfee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.0.43 +* Added autoDispose flag in BetterPlayerConfiguration +* Added removeEventsListener in BetterPlayerController +* Video list examples update +* Fixed Android native build warnings +* Fixed placeholder until play issues +* Added placeholderOnTop to the BetterPlayerConfiguration +* Lint fixes + ## 0.0.42 * Fixed resolution issue * Fixed type of BetterPlayerDataSource for file type diff --git a/README.md b/README.md index 6bbe03dc6..380182afd 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This plugin is based on [Chewie](https://github.com/brianegan/chewie). Chewie is ```yaml dependencies: - better_player: ^0.0.42 + better_player: ^0.0.43 ``` 2. Install it @@ -269,6 +269,11 @@ Possible configuration options: /// Should the placeholder be shown until play is pressed final bool showPlaceholderUntilPlay; + /// Placeholder position of player stack. If false, then placeholder will be + /// displayed on the bottom, so user need to hide it manually. Default is + /// true. + final bool placeholderOnTop; + /// A widget which is placed between the video and the controls final Widget overlay; @@ -324,6 +329,14 @@ Possible configuration options: ///[deviceOrientationsOnFullScreen] and [fullScreenAspectRatio] value will be /// ignored. final bool autoDetectFullscreenDeviceOrientation; + + ///Defines flag which enables/disables lifecycle handling (pause on app closed, + ///play on app resumed). Default value is true. + final bool handleLifecycle; + + ///Defines flag which enabled/disabled auto dispose on BetterPlayer dispose. + ///Default value is true. + final bool autoDispose; ``` ### BetterPlayerSubtitlesConfiguration diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java index 470d4ee81..64548e5f1 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java @@ -18,7 +18,6 @@ import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; -import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -52,7 +51,6 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.view.TextureRegistry; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -73,25 +71,15 @@ final class BetterPlayer { private static final String DEFAULT_NOTIFICATION_CHANNEL = "BETTER_PLAYER_NOTIFICATION"; private static final int NOTIFICATION_ID = 20772077; - private SimpleExoPlayer exoPlayer; - - private Surface surface; - + private final SimpleExoPlayer exoPlayer; + private final DefaultTrackSelector trackSelector; private final TextureRegistry.SurfaceTextureEntry textureEntry; - - private QueuingEventSink eventSink = new QueuingEventSink(); - + private final QueuingEventSink eventSink = new QueuingEventSink(); private final EventChannel eventChannel; private boolean isInitialized = false; - + private Surface surface; private String key; - - private DefaultTrackSelector trackSelector; - - private long maxCacheSize; - private long maxCacheFileSize; - private PlayerNotificationManager playerNotificationManager; private Handler refreshHandler; private Runnable refreshRunnable; @@ -175,24 +163,22 @@ public String getCurrentContentText(Player player) { @Nullable @Override public Bitmap getCurrentLargeIcon(Player player, PlayerNotificationManager.BitmapCallback callback) { - if (imageUrl == null){ + if (imageUrl == null) { return null; } - if (bitmap != null){ + if (bitmap != null) { return bitmap; } new Thread(() -> { bitmap = null; - if (imageUrl.contains("http")){ + if (imageUrl.contains("http")) { bitmap = getBitmapFromExternalURL(imageUrl); } else { bitmap = getBitmapFromInternalURL(imageUrl); } Bitmap finalBitmap = bitmap; - new Handler(Looper.getMainLooper()).post(() -> { - callback.onBitmap(finalBitmap); - }); + new Handler(Looper.getMainLooper()).post(() -> callback.onBitmap(finalBitmap)); }).start(); return null; @@ -246,7 +232,7 @@ public void onCommand(String command, Bundle extras, ResultReceiver cb) { playerNotificationManager.setControlDispatcher(new ControlDispatcher() { @Override public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { - String eventType = ""; + String eventType; if (player.getPlayWhenReady()) { eventType = "pause"; } else { @@ -287,25 +273,22 @@ public boolean dispatchStop(Player player, boolean reset) { }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { refreshHandler = new Handler(); - refreshRunnable = new Runnable() { - @Override - public void run() { - PlaybackStateCompat playbackState = null; - if (exoPlayer.getPlayWhenReady()) { - playbackState = new PlaybackStateCompat.Builder() - .setActions(PlaybackStateCompat.ACTION_SEEK_TO) - .setState(PlaybackStateCompat.STATE_PAUSED, getPosition(), 1.0f) - .build(); - } else { - playbackState = new PlaybackStateCompat.Builder() - .setActions(PlaybackStateCompat.ACTION_SEEK_TO) - .setState(PlaybackStateCompat.STATE_PLAYING, getPosition(), 1.0f) - .build(); - } - - mediaSession.setPlaybackState(playbackState); - refreshHandler.postDelayed(refreshRunnable, 1000); + refreshRunnable = () -> { + PlaybackStateCompat playbackState; + if (exoPlayer.getPlayWhenReady()) { + playbackState = new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SEEK_TO) + .setState(PlaybackStateCompat.STATE_PAUSED, getPosition(), 1.0f) + .build(); + } else { + playbackState = new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_SEEK_TO) + .setState(PlaybackStateCompat.STATE_PLAYING, getPosition(), 1.0f) + .build(); } + + mediaSession.setPlaybackState(playbackState); + refreshHandler.postDelayed(refreshRunnable, 1000); }; refreshHandler.postDelayed(refreshRunnable, 0); } @@ -323,27 +306,27 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { exoPlayer.seekTo(0); } - public void removeNotificationData(){ + public void removeNotificationData() { exoPlayer.removeListener(exoPlayerEventListener); if (refreshHandler != null) { refreshHandler.removeCallbacksAndMessages(null); refreshHandler = null; refreshRunnable = null; } - if (playerNotificationManager != null){ + if (playerNotificationManager != null) { playerNotificationManager.setPlayer(null); } bitmap = null; } - private static Bitmap getBitmapFromInternalURL(String src){ + private static Bitmap getBitmapFromInternalURL(String src) { try { - File file = new File(src); return BitmapFactory.decodeFile(src); - } catch (Exception exception){ + } catch (Exception exception) { return null; } } + private static Bitmap getBitmapFromExternalURL(String src) { try { URL url = new URL(src); @@ -351,8 +334,7 @@ private static Bitmap getBitmapFromExternalURL(String src) { connection.setDoInput(true); connection.connect(); InputStream input = connection.getInputStream(); - Bitmap myBitmap = BitmapFactory.decodeStream(input); - return myBitmap; + return BitmapFactory.decodeStream(input); } catch (IOException exception) { return null; } diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java index 4bc085e27..c290c2b0a 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java @@ -8,6 +8,7 @@ import android.util.Log; import android.util.LongSparseArray; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -108,12 +109,12 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { binding.getBinaryMessenger(), FlutterMain::getLookupKeyForAsset, FlutterMain::getLookupKeyForAsset, - binding.getFlutterEngine().getRenderer()); + binding.getTextureRegistry()); flutterState.startListening(this); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { if (flutterState == null) { Log.wtf(TAG, "Detached from the engine before registering to it."); } @@ -140,7 +141,7 @@ private void onDestroy() { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { if (flutterState == null || flutterState.textureRegistry == null) { result.error("no_activity", "better_player plugin requires a foreground activity", null); return; @@ -211,14 +212,14 @@ private void onMethodCall(MethodCall call, Result result, long textureId, Better player.sendBufferingUpdate(); break; case SET_SPEED_METHOD: - player.setSpeed((Double) call.argument(SPEED_PARAMETER)); + player.setSpeed(call.argument(SPEED_PARAMETER)); result.success(null); break; case SET_TRACK_PARAMETERS_METHOD: player.setTrackParameters( - (Integer) call.argument(WIDTH_PARAMETER), - (Integer) call.argument(HEIGHT_PARAMETER), - (Integer) call.argument(BITRATE_PARAMETER)); + call.argument(WIDTH_PARAMETER), + call.argument(HEIGHT_PARAMETER), + call.argument(BITRATE_PARAMETER)); result.success(null); break; case DISPOSE_METHOD: @@ -321,8 +322,8 @@ private void removeOtherNotificationListeners() { } } - - private T getParameter(Map parameters, Object key, T defaultValue) { + @SuppressWarnings("unchecked") + private T getParameter(Map parameters, String key, T defaultValue) { if (parameters.containsKey(key)) { Object value = parameters.get(key); if (value != null) { diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index aa5bb4120..b04f5e26b 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 3bda546e5..91ea4a65e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -29,4 +29,5 @@ + diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5a2f14fb1..44e62bcf0 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/lib/pages/video_list/video_list_data.dart b/example/lib/model/video_list_data.dart similarity index 100% rename from example/lib/pages/video_list/video_list_data.dart rename to example/lib/model/video_list_data.dart diff --git a/example/lib/pages/fade_placeholder_page.dart b/example/lib/pages/fade_placeholder_page.dart new file mode 100644 index 000000000..c6763b53b --- /dev/null +++ b/example/lib/pages/fade_placeholder_page.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:better_player/better_player.dart'; +import 'package:better_player_example/constants.dart'; +import 'package:flutter/material.dart'; + +class FadePlaceholderPage extends StatefulWidget { + @override + _FadePlaceholderPageState createState() => _FadePlaceholderPageState(); +} + +class _FadePlaceholderPageState extends State { + BetterPlayerController _betterPlayerController; + StreamController _playController = StreamController.broadcast(); + + @override + void initState() { + BetterPlayerConfiguration betterPlayerConfiguration = + BetterPlayerConfiguration( + aspectRatio: 16 / 9, + fit: BoxFit.contain, + placeholder: _buildPlaceholder(), + showPlaceholderUntilPlay: true, + placeholderOnTop: false, + ); + BetterPlayerDataSource dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.forBiggerBlazesUrl, + ); + _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); + _betterPlayerController.setupDataSource(dataSource); + _betterPlayerController.addEventsListener((event) { + if (event.betterPlayerEventType == BetterPlayerEventType.play) { + _playController.add(false); + } + }); + super.initState(); + } + + Widget _buildPlaceholder() { + return StreamBuilder( + stream: _playController.stream, + builder: (context, snapshot) { + bool showPlaceholder = snapshot.data ?? true; + return AnimatedOpacity( + duration: Duration(milliseconds: 500), + opacity: showPlaceholder ? 1.0 : 0.0, + child: AspectRatio( + aspectRatio: 16 / 9, + child: Image.network( + "https://img.webmd.com/dtmcms/live/webmd/consumer_assets/site_images/article_thumbnails/other/cat_relaxing_on_patio_other/1800x1200_cat_relaxing_on_patio_other.jpg", + fit: BoxFit.fill, + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Fade placeholder player"), + ), + body: Column( + children: [ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Normal player with configuration managed by developer.", + style: TextStyle(fontSize: 16), + ), + ), + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _betterPlayerController), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/reusable_video_list/reusable_video_list_controller.dart b/example/lib/pages/reusable_video_list/reusable_video_list_controller.dart new file mode 100644 index 000000000..6b1582c11 --- /dev/null +++ b/example/lib/pages/reusable_video_list/reusable_video_list_controller.dart @@ -0,0 +1,40 @@ +import 'package:better_player/better_player.dart'; + +class ReusableVideoListController { + final List _betterPlayerControllerRegistry = []; + final List _usedBetterPlayerControllerRegistry = []; + + ReusableVideoListController() { + for (int index = 0; index < 3; index++) { + _betterPlayerControllerRegistry.add( + BetterPlayerController( + BetterPlayerConfiguration(handleLifecycle: false, autoDispose: false), + ), + ); + } + } + + BetterPlayerController getBetterPlayerController() { + final freeController = _betterPlayerControllerRegistry.firstWhere( + (controller) => + !_usedBetterPlayerControllerRegistry.contains(controller), + orElse: () => null); + + if (freeController != null) { + _usedBetterPlayerControllerRegistry.add(freeController); + } + + return freeController; + } + + void freeBetterPlayerController( + BetterPlayerController betterPlayerController) { + _usedBetterPlayerControllerRegistry.remove(betterPlayerController); + } + + void dispose() { + _betterPlayerControllerRegistry.forEach((controller) { + controller.dispose(); + }); + } +} diff --git a/example/lib/pages/reusable_video_list/reusable_video_list_page.dart b/example/lib/pages/reusable_video_list/reusable_video_list_page.dart new file mode 100644 index 000000000..db826b133 --- /dev/null +++ b/example/lib/pages/reusable_video_list/reusable_video_list_page.dart @@ -0,0 +1,69 @@ +import 'dart:math'; + +import 'package:better_player_example/constants.dart'; +import 'package:better_player_example/model/video_list_data.dart'; +import 'package:better_player_example/pages/reusable_video_list/reusable_video_list_controller.dart'; +import 'package:better_player_example/pages/reusable_video_list/reusable_video_list_widget.dart'; +import 'package:flutter/material.dart'; + +class ReusableVideoListPage extends StatefulWidget { + @override + _ReusableVideoListPageState createState() => _ReusableVideoListPageState(); +} + +class _ReusableVideoListPageState extends State { + ReusableVideoListController videoListController = + ReusableVideoListController(); + final _random = new Random(); + final List _videos = [ + Constants.bugBuckBunnyVideoUrl, + Constants.forBiggerBlazesUrl, + Constants.forBiggerJoyridesVideoUrl, + Constants.elephantDreamVideoUrl, + ]; + List dataList = []; + var value = 0; + + @override + void initState() { + _setupData(); + super.initState(); + } + + void _setupData() { + for (int index = 0; index < 10; index++) { + var randomVideoUrl = _videos[_random.nextInt(_videos.length)]; + dataList.add(VideoListData("Video $index", randomVideoUrl)); + } + } + + @override + void dispose() { + videoListController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Reusable video list")), + body: Container( + color: Colors.grey, + child: Column(children: [ + Expanded( + child: ListView.builder( + itemCount: dataList.length, + itemBuilder: (context, index) { + VideoListData videoListData = dataList[index]; + return ReusableVideoListWidget( + videoListData: videoListData, + videoListController: videoListController, + ); + }, + ), + ) + ]), + ), + ); + } +} diff --git a/example/lib/pages/reusable_video_list/reusable_video_list_widget.dart b/example/lib/pages/reusable_video_list/reusable_video_list_widget.dart new file mode 100644 index 000000000..6669c95c5 --- /dev/null +++ b/example/lib/pages/reusable_video_list/reusable_video_list_widget.dart @@ -0,0 +1,174 @@ +import 'dart:async'; + +import 'package:better_player/better_player.dart'; +import 'package:better_player_example/model/video_list_data.dart'; +import 'package:better_player_example/pages/reusable_video_list/reusable_video_list_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widgets/flutter_widgets.dart'; + +class ReusableVideoListWidget extends StatefulWidget { + final VideoListData videoListData; + final ReusableVideoListController videoListController; + + const ReusableVideoListWidget({ + Key key, + this.videoListData, + this.videoListController, + }) : super(key: key); + + @override + _ReusableVideoListWidgetState createState() => + _ReusableVideoListWidgetState(); +} + +class _ReusableVideoListWidgetState extends State { + VideoListData get videoListData => widget.videoListData; + BetterPlayerController controller; + StreamController + betterPlayerControllerStreamController = StreamController.broadcast(); + bool _initialized = false; + bool _wasPlaying = false; + Duration _lastPosition; + bool _afterBuild = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _afterBuild = true; + }); + } + + @override + void dispose() { + betterPlayerControllerStreamController.close(); + super.dispose(); + } + + void _setupController() { + if (controller == null) { + controller = widget.videoListController.getBetterPlayerController(); + controller.setupDataSource( + BetterPlayerDataSource.network(videoListData.videoUrl)); + betterPlayerControllerStreamController.add(controller); + controller.addEventsListener(onPlayerEvent); + } + } + + void _freeController() { + if (!_initialized) { + _initialized = true; + return; + } + if (controller != null && _initialized) { + _afterBuild = false; + controller.removeEventsListener(onPlayerEvent); + _wasPlaying = controller.isPlaying(); + _lastPosition = controller.videoPlayerController.value.position; + widget.videoListController.freeBetterPlayerController(controller); + controller.pause(); + controller = null; + betterPlayerControllerStreamController.add(null); + _initialized = false; + } + } + + void onPlayerEvent(BetterPlayerEvent event) { + if (event.betterPlayerEventType == BetterPlayerEventType.initialized) { + if (_lastPosition != null) { + controller.seekTo(_lastPosition); + } + if (_wasPlaying) { + controller.play(); + } + } + } + + ///TODO: Handle "setState() or markNeedsBuild() called during build." error + ///when fast scrolling through the list + @override + Widget build(BuildContext context) { + return Card( + margin: EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(8), + child: Text( + videoListData.videoTitle, + style: TextStyle(fontSize: 50), + ), + ), + VisibilityDetector( + key: Key(hashCode.toString() + DateTime.now().toString()), + onVisibilityChanged: (info) { + if (!_afterBuild) { + return; + } + if (info.visibleFraction >= 0.8) { + _setupController(); + } else { + _freeController(); + } + }, + child: StreamBuilder( + stream: betterPlayerControllerStreamController.stream, + builder: (context, snapshot) { + return AspectRatio( + aspectRatio: 16 / 9, + child: controller != null + ? BetterPlayer( + controller: controller, + ) + : Container(), + ); + }, + ), + ), + Padding( + padding: EdgeInsets.all(8), + child: Text( + "Horror: In Steven Spielberg's Jaws, a shark terrorizes a beach " + "town. Plainspoken sheriff Roy Scheider, hippie shark " + "researcher Richard Dreyfuss, and a squirrely boat captain " + "set out to find the beast, but will they escape with their " + "lives? 70's special effects, legendary score, and trademark " + "humor set this classic apart."), + ), + Center( + child: Wrap(children: [ + RaisedButton( + child: Text("Play"), + onPressed: () { + controller.play(); + }, + ), + const SizedBox(width: 8), + RaisedButton( + child: Text("Pause"), + onPressed: () { + controller.pause(); + }, + ), + const SizedBox(width: 8), + RaisedButton( + child: Text("Set max volume"), + onPressed: () { + controller.setVolume(100); + }, + ), + ]), + ), + ], + ), + ); + } + + @override + void deactivate() { + _initialized = true; + _freeController(); + super.deactivate(); + } +} diff --git a/example/lib/pages/video_list/video_list_page.dart b/example/lib/pages/video_list/video_list_page.dart index 3c9f1f895..ac599466e 100644 --- a/example/lib/pages/video_list/video_list_page.dart +++ b/example/lib/pages/video_list/video_list_page.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:better_player_example/constants.dart'; -import 'package:better_player_example/pages/video_list/video_list_data.dart'; +import 'package:better_player_example/model/video_list_data.dart'; import 'package:flutter/material.dart'; import 'video_list_widget.dart'; diff --git a/example/lib/pages/video_list/video_list_widget.dart b/example/lib/pages/video_list/video_list_widget.dart index 7304849b5..7b007fe60 100644 --- a/example/lib/pages/video_list/video_list_widget.dart +++ b/example/lib/pages/video_list/video_list_widget.dart @@ -1,5 +1,5 @@ import 'package:better_player/better_player.dart'; -import 'package:better_player_example/pages/video_list/video_list_data.dart'; +import 'package:better_player_example/model/video_list_data.dart'; import 'package:flutter/material.dart'; class VideoListWidget extends StatefulWidget { diff --git a/example/lib/pages/welcome_page.dart b/example/lib/pages/welcome_page.dart index 862a9941f..71eac2f31 100644 --- a/example/lib/pages/welcome_page.dart +++ b/example/lib/pages/welcome_page.dart @@ -7,6 +7,7 @@ import 'package:better_player_example/pages/cache_page.dart'; import 'package:better_player_example/pages/controller_controls_page.dart'; import 'package:better_player_example/pages/controls_configuration_page.dart'; import 'package:better_player_example/pages/event_listener_page.dart'; +import 'package:better_player_example/pages/fade_placeholder_page.dart'; import 'package:better_player_example/pages/hls_subtitles_page.dart'; import 'package:better_player_example/pages/hls_tracks_page.dart'; import 'package:better_player_example/pages/memory_player_page.dart'; @@ -15,6 +16,7 @@ import 'package:better_player_example/pages/notification_player_page.dart'; import 'package:better_player_example/pages/overridden_aspect_ratio_page.dart'; import 'package:better_player_example/pages/playlist_page.dart'; import 'package:better_player_example/pages/resolutions_page.dart'; +import 'package:better_player_example/pages/reusable_video_list/reusable_video_list_page.dart'; import 'package:better_player_example/pages/rotation_and_fit_page.dart'; import 'package:better_player_example/pages/subtitles_page.dart'; import 'package:better_player_example/pages/video_list/video_list_page.dart'; @@ -120,6 +122,12 @@ class _WelcomePageState extends State { _buildExampleElementWidget("Notifications player page", () { _navigateToPage(NotificationPlayerPage()); }), + _buildExampleElementWidget("Reusable video list page", () { + _navigateToPage(ReusableVideoListPage()); + }), + _buildExampleElementWidget("Fade placeholder page", () { + _navigateToPage(FadePlaceholderPage()); + }), ]; } diff --git a/lib/src/configuration/better_player_configuration.dart b/lib/src/configuration/better_player_configuration.dart index a9daa48a2..a6dadeaf4 100644 --- a/lib/src/configuration/better_player_configuration.dart +++ b/lib/src/configuration/better_player_configuration.dart @@ -36,6 +36,11 @@ class BetterPlayerConfiguration { /// Should the placeholder be shown until play is pressed final bool showPlaceholderUntilPlay; + /// Placeholder position of player stack. If false, then placeholder will be + /// displayed on the bottom, so user need to hide it manually. Default is + /// true. + final bool placeholderOnTop; + /// A widget which is placed between the video and the controls final Widget overlay; @@ -96,39 +101,46 @@ class BetterPlayerConfiguration { ///play on app resumed). Default value is true. final bool handleLifecycle; - const BetterPlayerConfiguration( - {this.aspectRatio, - this.autoPlay = false, - this.startAt, - this.looping = false, - this.fullScreenByDefault = false, - this.placeholder, - this.showPlaceholderUntilPlay = false, - this.overlay, - this.errorBuilder, - this.allowedScreenSleep = true, - this.fullScreenAspectRatio, - this.deviceOrientationsOnFullScreen = const [ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ], - this.systemOverlaysAfterFullScreen = SystemUiOverlay.values, - this.deviceOrientationsAfterFullScreen = const [ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ], - this.routePageBuilder, - this.eventListener, - this.subtitlesConfiguration = const BetterPlayerSubtitlesConfiguration(), - this.controlsConfiguration = const BetterPlayerControlsConfiguration(), - this.fit = BoxFit.fill, - this.rotation = 0, - this.playerVisibilityChangedBehavior, - this.translations, - this.autoDetectFullscreenDeviceOrientation = false, - this.handleLifecycle = true}); + ///Defines flag which enabled/disabled auto dispose on BetterPlayer dispose. + ///Default value is true. + final bool autoDispose; + + const BetterPlayerConfiguration({ + this.aspectRatio, + this.autoPlay = false, + this.startAt, + this.looping = false, + this.fullScreenByDefault = false, + this.placeholder, + this.showPlaceholderUntilPlay = false, + this.placeholderOnTop = true, + this.overlay, + this.errorBuilder, + this.allowedScreenSleep = true, + this.fullScreenAspectRatio, + this.deviceOrientationsOnFullScreen = const [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ], + this.systemOverlaysAfterFullScreen = SystemUiOverlay.values, + this.deviceOrientationsAfterFullScreen = const [ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ], + this.routePageBuilder, + this.eventListener, + this.subtitlesConfiguration = const BetterPlayerSubtitlesConfiguration(), + this.controlsConfiguration = const BetterPlayerControlsConfiguration(), + this.fit = BoxFit.fill, + this.rotation = 0, + this.playerVisibilityChangedBehavior, + this.translations, + this.autoDetectFullscreenDeviceOrientation = false, + this.handleLifecycle = true, + this.autoDispose = true, + }); BetterPlayerConfiguration copyWith({ double aspectRatio, @@ -137,6 +149,8 @@ class BetterPlayerConfiguration { bool looping, bool fullScreenByDefault, Widget placeholder, + bool showPlaceholderUntilPlay, + bool placeholderOnTop, Widget overlay, bool showControlsOnInitialize, Widget Function(BuildContext context, String errorMessage) errorBuilder, @@ -162,6 +176,9 @@ class BetterPlayerConfiguration { looping: looping ?? this.looping, fullScreenByDefault: fullScreenByDefault ?? this.fullScreenByDefault, placeholder: placeholder ?? this.placeholder, + showPlaceholderUntilPlay: + showPlaceholderUntilPlay ?? this.showPlaceholderUntilPlay, + placeholderOnTop: placeholderOnTop ?? this.placeholderOnTop, overlay: overlay ?? this.overlay, errorBuilder: errorBuilder ?? this.errorBuilder, allowedScreenSleep: allowedScreenSleep ?? this.allowedScreenSleep, diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index cbe7159cc..f806ef51a 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -473,6 +473,10 @@ class BetterPlayerController extends ChangeNotifier { _eventListeners.add(eventListener); } + void removeEventsListener(Function(BetterPlayerEvent) eventListener) { + _eventListeners.remove(eventListener); + } + bool isLiveStream() { return _betterPlayerDataSource?.liveStream == true; } @@ -629,6 +633,9 @@ class BetterPlayerController extends ChangeNotifier { @override void dispose() { + if (!betterPlayerConfiguration.autoDispose) { + return; + } if (!_disposed) { _eventListeners.clear(); videoPlayerController?.removeListener(_fullScreenListener); diff --git a/lib/src/core/better_player_with_controls.dart b/lib/src/core/better_player_with_controls.dart index 4d8f947f5..2c17ebfd4 100644 --- a/lib/src/core/better_player_with_controls.dart +++ b/lib/src/core/better_player_with_controls.dart @@ -118,13 +118,15 @@ class _BetterPlayerWithControlsState extends State { } _initalized = true; + final bool placeholderOnTop = + betterPlayerController.betterPlayerConfiguration.placeholderOnTop ?? + false; // ignore: avoid_unnecessary_containers return Container( child: Stack( fit: StackFit.passthrough, children: [ - betterPlayerController.betterPlayerConfiguration.placeholder ?? - Container(), + if (placeholderOnTop) _buildPlaceholder(betterPlayerController), Transform.rotate( angle: rotation * pi / 180, child: _BetterPlayerVideoFitWidget( @@ -140,12 +142,18 @@ class _BetterPlayerWithControlsState extends State { subtitles: betterPlayerController.subtitlesLines, playerVisibilityStream: playerVisibilityStreamController.stream, ), + if (!placeholderOnTop) _buildPlaceholder(betterPlayerController), _buildControls(context, betterPlayerController), ], ), ); } + Widget _buildPlaceholder(BetterPlayerController betterPlayerController) { + return betterPlayerController.betterPlayerConfiguration.placeholder ?? + Container(); + } + Widget _buildControls( BuildContext context, BetterPlayerController betterPlayerController, @@ -247,7 +255,8 @@ class _BetterPlayerVideoFitWidgetState if (event.betterPlayerEventType == BetterPlayerEventType.play) { if (widget.betterPlayerController.betterPlayerConfiguration .showPlaceholderUntilPlay && - !_started) { + !_started && + mounted) { setState(() { _started = widget.betterPlayerController.hasCurrentDataSourceStarted; diff --git a/lib/src/video_player/method_channel_video_player.dart b/lib/src/video_player/method_channel_video_player.dart index a9c5fa559..c483c0b04 100644 --- a/lib/src/video_player/method_channel_video_player.dart +++ b/lib/src/video_player/method_channel_video_player.dart @@ -201,18 +201,18 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { try { if (map.containsKey("width")) { - num widthNum = map["width"]; + final num widthNum = map["width"] as num; width = widthNum.toDouble(); } if (map.containsKey("height")) { - num heightNum = map["height"]; + final num heightNum = map["height"] as num; height = heightNum.toDouble(); } } catch (exception) { - BetterPlayerUtils.log(exception); + BetterPlayerUtils.log(exception.toString()); } - Size size = Size(width, height); + final Size size = Size(width, height); return VideoEvent( eventType: VideoEventType.initialized, diff --git a/pubspec.yaml b/pubspec.yaml index 3b6f82063..02262f83f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: better_player description: Advanced video player based on video_player and Chewie. It's solves many typical use cases and it's easy to run. -version: 0.0.42 +version: 0.0.43 authors: - Jakub Homlala homepage: https://github.com/jhomlala/betterplayer