diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d935b88..cfcc3c5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.0.71 +* Fixed play after seeking issue on iOS. +* Fixed audio track selection issue on iOS/Android. +* Fixed issue where speed which couldn't be applied on iOS was saved in player state. +* Added support for D-pad navigation using a Android TV remote control (by https://github.com/danielz-nenda) +* Added `BetterPlayerMultipleGestureDetector` to handle problems with gesture detection +* Expose getter for `eventListeners` in `BetterPlayerController` (by https://github.com/Letalus) +* Updated documentation +* Updated dependencies + ## 0.0.70 * Fixed file data source exception. Right now user will be only warned. * Fixed playback speed after seek in iOS. diff --git a/README.md b/README.md index ec74f98ef..9d3264555 100644 --- a/README.md +++ b/README.md @@ -89,1022 +89,14 @@ This plugin is based on [Chewie](https://github.com/brianegan/chewie). Chewie is ✔️ ... and much more! -## Install +## Documentation +* [Official documentation](https://github.com/jhomlala/betterplayer) +* [Example application](https://github.com/jhomlala/betterplayer/tree/master/example) +* [API reference](https://pub.dev/documentation/better_player/latest/better_player/better_player-library.html) -1. Add this to your **pubspec.yaml** file: - -```yaml -dependencies: - better_player: ^0.0.70 -``` - -2. Install it - -```bash -$ flutter packages get -``` - -3. Import it - -```dart -import 'package:better_player/better_player.dart'; -``` ## Important information This plugin development is in progress. You may encounter breaking changes each version. This plugin is developed part-time for free. If you need some feature which is supported by other players available in pub dev, then feel free to create PR. All valuable contributions are welcome! -## General Usage -Check [Example project](https://github.com/jhomlala/betterplayer/tree/master/example) which shows how to use Better Player in different scenarios. - -### Basic usage -There are 2 basic methods which you can use to setup Better Player: -```dart -BetterPlayer.network(url, configuration) -BetterPlayer.file(url, configuration) -``` -There methods setup basic configuration for you and allows you to start using player in few seconds. -Here is an example: -```dart - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Example player"), - ), - body: AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer.network( - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", - betterPlayerConfiguration: BetterPlayerConfiguration( - aspectRatio: 16 / 9, - ), - ), - ), - ); - } -``` -In this example, we're just showing video from url with aspect ratio = 16/9. -Better Player has many more configuration options which are presented below. - - -### Normal usage - -Create BetterPlayerDataSource and BetterPlayerController. You should do it in initState: -```dart -BetterPlayerController _betterPlayerController; - - @override - void initState() { - super.initState(); - BetterPlayerDataSource betterPlayerDataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"); - _betterPlayerController = BetterPlayerController( - BetterPlayerConfiguration(), - betterPlayerDataSource: betterPlayerDataSource); - } -```` - -Create BetterPlayer widget wrapped in AspectRatio widget: -```dart - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer( - controller: _betterPlayerController, - ), - ); - } -``` - -### Playlist -To use playlist, you need to create dataset with multiple videos: -```dart - List createDataSet() { - List dataSourceList = List(); - dataSourceList.add( - BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", - ), - ); - dataSourceList.add( - BetterPlayerDataSource(BetterPlayerDataSourceType.network, - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"), - ); - dataSourceList.add( - BetterPlayerDataSource(BetterPlayerDataSourceType.network, - "http://sample.vodobox.com/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8"), - ); - return dataSourceList; - } -``` - -Then create BetterPlayerPlaylist: -```dart -@override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayerPlaylist( - betterPlayerConfiguration: BetterPlayerConfiguration(), - betterPlayerPlaylistConfiguration: const BetterPlayerPlaylistConfiguration(), - betterPlayerDataSourceList: dataSourceList), - ); - } -``` - -### BetterPlayerListViewPlayer -BetterPlayerListViewPlayer will auto play/pause video once video is visible on screen with playFraction. PlayFraction describes percent of video that must be visibile to play video. If playFraction is 0.8 then 80% of video height must be visible on screen to auto play video - -```dart - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayerListVideoPlayer( - BetterPlayerDataSource( - BetterPlayerDataSourceType.network, videoListData.videoUrl), - key: Key(videoListData.hashCode.toString()), - playFraction: 0.8, - ), - ); - } -``` - -You can control BetterPlayerListViewPlayer with BetterPlayerListViewPlayerController. You need to pass -BetterPlayerListViewPlayerController to BetterPlayerListVideoPlayer. See more in example app. - -### Subtitles -Subtitles can be configured from 3 different sources: file, network and memory. Subtitles source is passed in BetterPlayerDataSource: - -Network subtitles: -```dart - var dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", - subtitles: BetterPlayerSubtitlesSource.single( - type: BetterPlayerSubtitlesSourceType.network, - url: - "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt"), - ); -``` - -File subtitles: -```dart - var dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.file, - "${directory.path}/testvideo.mp4", - subtitles: BetterPlayerSubtitlesSource.single( - type: BetterPlayerSubtitlesSourceType.file, - url: "${directory.path}/example_subtitles.srt", - ), - ); -``` -You can pass multiple subtitles for one video: -```dart -var dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8", - liveStream: false, - useAsmsSubtitles: true, - hlsTrackNames: ["Low quality", "Not so low quality", "Medium quality"], - subtitles: [ - BetterPlayerSubtitlesSource( - type: BetterPlayerSubtitlesSourceType.network, - name: "EN", - urls: [ - "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt" - ], - ), - - BetterPlayerSubtitlesSource( - type: BetterPlayerSubtitlesSourceType.network, - name: "DE", - urls: [ - "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt" - ], - ), - ], - ); -``` - -### BetterPlayerConfiguration -You can provide configuration to your player when creating BetterPlayerController. - -```dart - var betterPlayerConfiguration = BetterPlayerConfiguration( - autoPlay: true, - looping: true, - fullScreenByDefault: true, - ); -``` - -Possible configuration options: -```dart - /// Play the video as soon as it's displayed - final bool autoPlay; - - /// Start video at a certain position - final Duration startAt; - - /// Whether or not the video should loop - final bool looping; - - /// Weather or not to show the controls when initializing the widget. - final bool showControlsOnInitialize; - - /// When the video playback runs into an error, you can build a custom - /// error message. - final Widget Function(BuildContext context, String errorMessage) errorBuilder; - - /// The Aspect Ratio of the Video. Important to get the correct size of the - /// video! - /// - /// Will fallback to fitting within the space allowed. - final double aspectRatio; - - /// The placeholder is displayed underneath the Video before it is initialized - /// or played. - final Widget placeholder; - - /// 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; - - /// Defines if the player will start in fullscreen when play is pressed - final bool fullScreenByDefault; - - /// Defines if the player will sleep in fullscreen or not - final bool allowedScreenSleep; - - /// Defines aspect ratio which will be used in fullscreen - final double fullScreenAspectRatio; - - /// Defines the set of allowed device orientations on entering fullscreen - final List deviceOrientationsOnFullScreen; - - /// Defines the system overlays visible after exiting fullscreen - final List systemOverlaysAfterFullScreen; - - /// Defines the set of allowed device orientations after exiting fullscreen - final List deviceOrientationsAfterFullScreen; - - /// Defines a custom RoutePageBuilder for the fullscreen - final BetterPlayerRoutePageBuilder routePageBuilder; - - /// Defines a event listener where video player events will be send - final Function(BetterPlayerEvent) eventListener; - - ///Defines subtitles configuration - final BetterPlayerSubtitlesConfiguration subtitlesConfiguration; - - ///Defines controls configuration - final BetterPlayerControlsConfiguration controlsConfiguration; - - ///Defines fit of the video, allows to fix video stretching, see possible - ///values here: https://api.flutter.dev/flutter/painting/BoxFit-class.html - final BoxFit fit; - - ///Defines rotation of the video in degrees. Default value is 0. Can be 0, 90, 180, 270. - ///Angle will rotate only video box, controls will be in the same place. - final double rotation; - - ///Defines function which will react on player visibility changed - final Function(double visibilityFraction) playerVisibilityChangedBehavior; - - ///Defines translations used in player. If null, then default english translations - ///will be used. - final List translations; - - ///Defines if player should auto detect full screen device orientation based - ///on aspect ratio of the video. If aspect ratio of the video is < 1 then - ///video will played in full screen in portrait mode. If aspect ratio is >= 1 - ///then video will be played horizontally. If this parameter is true, then - ///[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 -You can provide subtitles configuration with this class. You should put BetterPlayerSubtitlesConfiguration in BetterPlayerConfiguration. -```dart - var betterPlayerConfiguration = BetterPlayerConfiguration( - subtitlesConfiguration: BetterPlayerSubtitlesConfiguration( - fontSize: 20, - fontColor: Colors.green, - ), - ); -``` - -Possible configuration options: -```dart - ///Subtitle font size - final double fontSize; - - ///Subtitle font color - final Color fontColor; - - ///Enable outline (border) of the text - final bool outlineEnabled; - - ///Color of the outline stroke - final Color outlineColor; - - ///Outline stroke size - final double outlineSize; - - ///Font family of the subtitle - final String fontFamily; - - ///Left padding of the subtitle - final double leftPadding; - - ///Right padding of the subtitle - final double rightPadding; - - ///Bottom padding of the subtitle - final double bottomPadding; - - ///Alignment of the subtitle - final Alignment alignment; - - ///Background color of the subtitle - final Color backgroundColor; - - ///Subtitles selected by default, without user interaction - final bool selectedByDefault; -``` - -### BetterPlayerControlsConfiguration -Configuration for player GUI. You should pass this configuration to BetterPlayerConfiguration. - -```dart -var betterPlayerConfiguration = BetterPlayerConfiguration( - controlsConfiguration: BetterPlayerControlsConfiguration( - textColor: Colors.black, - iconsColor: Colors.black, - ), - ); -``` - - -```dart - ///Color of the control bars - final Color controlBarColor; - - ///Color of texts - final Color textColor; - - ///Color of icons - final Color iconsColor; - - ///Icon of play - final IconData playIcon; - - ///Icon of pause - final IconData pauseIcon; - - ///Icon of mute - final IconData muteIcon; - - ///Icon of unmute - final IconData unMuteIcon; - - ///Icon of fullscreen mode enable - final IconData fullscreenEnableIcon; - - ///Icon of fullscreen mode disable - final IconData fullscreenDisableIcon; - - ///Cupertino only icon, icon of skip - final IconData skipBackIcon; - - ///Cupertino only icon, icon of forward - final IconData skipForwardIcon; - - ///Flag used to enable/disable fullscreen - final bool enableFullscreen; - - ///Flag used to enable/disable mute - final bool enableMute; - - ///Flag used to enable/disable progress texts - final bool enableProgressText; - - ///Flag used to enable/disable progress bar - final bool enableProgressBar; - - ///Flag used to enable/disable progress bar drag - final bool enableProgressBarDrag; - - ///Flag used to enable/disable play-pause - final bool enablePlayPause; - - ///Flag used to enable skip forward and skip back - final bool enableSkips; - - ///Progress bar played color - final Color progressBarPlayedColor; - - ///Progress bar circle color - final Color progressBarHandleColor; - - ///Progress bar buffered video color - final Color progressBarBufferedColor; - - ///Progress bar background color - final Color progressBarBackgroundColor; - - ///Time to hide controls - final Duration controlsHideTime; - - ///Parameter used to build custom controls - final Widget Function(BetterPlayerController controller) - customControlsBuilder; - - ///Parameter used to change theme of the player - final BetterPlayerTheme playerTheme; - - ///Flag used to show/hide controls - final bool showControls; - - ///Flag used to show controls on init - final bool showControlsOnInitialize; - - ///Control bar height - final double controlBarHeight; - - ///Live text color; - final Color liveTextColor; - - ///Flag used to show/hide overflow menu which contains playback, subtitles, - ///qualities options. - final bool enableOverflowMenu; - - ///Flag used to show/hide playback speed - final bool enablePlaybackSpeed; - - ///Flag used to show/hide subtitles - final bool enableSubtitles; - - ///Flag used to show/hide qualities - final bool enableQualities; - - ///Flag used to show/hide PiP mode - final bool enablePip; - - ///Flag used to enable/disable retry feature - final bool enableRetry; - - ///Flag used to show/hide audio tracks - final bool enableAudioTracks; - - ///Custom items of overflow menu - final List overflowMenuCustomItems; - - ///Icon of the overflow menu - final IconData overflowMenuIcon; - - ///Icon of the playback speed menu item from overflow menu - final IconData playbackSpeedIcon; - - ///Icon of the subtitles menu item from overflow menu - final IconData subtitlesIcon; - - ///Icon of the qualities menu item from overflow menu - final IconData qualitiesIcon; - - ///Icon of the audios menu item from overflow menu - final IconData audioTracksIcon; - - ///Color of overflow menu icons - final Color overflowMenuIconsColor; - - ///Time which will be used once user uses forward - final int forwardSkipTimeInMilliseconds; - - ///Time which will be used once user uses backward - final int backwardSkipTimeInMilliseconds; - - ///Color of default loading indicator - final Color loadingColor; - - ///Widget which can be used instead of default progress - final Widget loadingWidget; - - ///Color of the background, when no frame is displayed. - final Color backgroundColor; -``` - -### BetterPlayerPlaylistConfiguration -Configure your playlist. Pass this object to BetterPlayerPlaylist - -```dart - var betterPlayerPlaylistConfiguration = BetterPlayerPlaylistConfiguration( - loopVideos: false, - nextVideoDelay: Duration(milliseconds: 5000), - ); -``` - -Possible configuration options: -```dart - ///How long user should wait for next video - final Duration nextVideoDelay; - - ///Should videos be looped - final bool loopVideos; - - ///Index of video that will start on playlist start. Id must be less than - ///elements in data source list. Default is 0. - final int initialStartIndex; -``` - -### BetterPlayerDataSource -Define source for one video in your app. There are 3 types of data sources: -* Network - data source which uses url to play video from external resources -* File - data source which uses url to play video from internal resources -* Memory - data source which uses list of bytes to play video from memory -```dart - var dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", - subtitles: BetterPlayerSubtitlesSource( - type: BetterPlayerSubtitlesSourceType.file, - url: "${directory.path}/example_subtitles.srt", - ), - headers: {"header":"my_custom_header"} - ); -``` - -You can use type specific factories to build your data source. -Use BetterPlayerDataSource.network to build network data source, BetterPlayerDataSource.file to build file data source and BetterPlayerDataSource.memory -to build memory data source. - -Possible configuration options: -```dart - ///Type of source of video - final BetterPlayerDataSourceType type; - - ///Url of the video - final String url; - - ///Subtitles configuration - ///You can pass here multiple subtitles - final List subtitles; - - ///Flag to determine if current data source is live stream - final bool liveStream; - - /// Custom headers for player - final Map headers; - - ///Should player use hls / dash subtitles (ASMS - Adaptive Streaming Media Sources). - final bool useAsmsSubtitles; - - ///Should player use hls tracks - final bool useAsmsTracks; - - ///Should player use hls /das audio tracks - final bool useAsmsAudioTracks; - - ///List of strings that represents tracks names. - ///If empty, then better player will choose name based on track parameters - final List hlsTrackNames; - - ///Optional, alternative resolutions for non-hls video. Used to setup - ///different qualities for video. - ///Data should be in given format: - ///{"360p": "url", "540p": "url2" } - final Map resolutions; - - ///Optional cache configuration, used only for network data sources - final BetterPlayerCacheConfiguration cacheConfiguration; - - ///List of bytes, used only in memory player - final List bytes; - - ///Configuration of remote controls notification - final BetterPlayerNotificationConfiguration notificationConfiguration; - - ///Duration which will be returned instead of original duration - final Duration overriddenDuration; - - ///Video format hint when data source url has not valid extension. - final BetterPlayerVideoFormat videoFormat; - - ///Extension of video without dot. Used only in memory data source. - final String videoExtension; - - ///Configuration of content protection - final BetterPlayerDrmConfiguration drmConfiguration; - - ///Placeholder widget which will be shown until video load or play. This - ///placeholder may be useful if you want to show placeholder before each video - ///in playlist. Otherwise, you should use placeholder from - /// BetterPlayerConfiguration. - final Widget placeholder; -``` - - -### BetterPlayerCacheConfiguration -Define cache configuration for given data source. Cache works only for network data sources. -```dart - ///Enable cache for network data source - final bool useCache; - - /// The maximum cache size to keep on disk in bytes. - /// Android only option. - final int maxCacheSize; - - /// The maximum size of each individual file in bytes. - /// Android only option. - final int maxCacheFileSize; - - ///Cache key to re-use same cached data between app sessions. - final String? key; -``` - - -### BetterPlayerSubtitlesSource -Define source of subtitles in your video: -```dart - var subtitles = BetterPlayerSubtitlesSource( - type: BetterPlayerSubtitlesSourceType.file, - url: "${directory.path}/example_subtitles.srt", - ); -``` - -Possible configuration options: -```dart - ///Source type - final BetterPlayerSubtitlesSourceType type; - - ///Url of the subtitles, used with file or network subtitles - final String url; - - ///Content of subtitles, used when type is memory - final String content; -``` - -### BetterPlayerBufferingConfiguration -Configuration class used to setup better buffering experience or setup custom load settings. Currently used only in Android. - -Possible configuration options: -```dart - ///The default minimum duration of media that the player will attempt to - ///ensure is buffered at all times, in milliseconds. - final int minBufferMs; - - ///The default maximum duration of media that the player will attempt to - ///buffer, in milliseconds. - final int maxBufferMs; - - ///The default duration of media that must be buffered for playback to start - ///or resume following a user action such as a seek, in milliseconds. - final int bufferForPlaybackMs; - - ///The default duration of media that must be buffered for playback to resume - ///after a rebuffer, in milliseconds. A rebuffer is defined to be caused by - ///buffer depletion rather than a user action. - final int bufferForPlaybackAfterRebufferMs; -``` - - -### BetterPlayerTranslations -You can provide translations for different languages. You need to pass list of BetterPlayerTranslations to -the BetterPlayerConfiguration. Here is an example: - -```dart - translations: [ - BetterPlayerTranslations( - languageCode: "language_code for example pl", - generalDefaultError: "translated text", - generalNone: "translated text", - generalDefault: "translated text", - playlistLoadingNextVideo: "translated text", - controlsLive: "translated text", - controlsNextVideoIn: "translated text", - overflowMenuPlaybackSpeed: "translated text", - overflowMenuSubtitles: "translated text", - overflowMenuQuality: "translated text", - ), - BetterPlayerTranslations( - languageCode: "other language for example cz", - generalDefaultError: "translated text", - generalNone: "translated text", - generalDefault: "translated text", - playlistLoadingNextVideo: "translated text", - controlsLive: "translated text", - controlsNextVideoIn: "translated text", - overflowMenuPlaybackSpeed: "translated text", - overflowMenuSubtitles: "translated text", - overflowMenuQuality: "translated text", - ), - ], -``` -There are 4 pre build in languages: EN, PL, ZH (chinese simplified), HI (hindi). If you didn't provide -any translation then EN translations will be used or any of the pre build in translations, only if it's -match current user locale. - -You need to setup localizations in your app first to make it work. Here's how you can do that: -https://flutter.dev/docs/development/accessibility-and-localization/internationalization - -### Listen to video events -You can listen to video player events like: -```dart - initialized, - play, - pause, - seekTo, - openFullscreen, - hideFullscreen, - setVolume, - progress, - finished, - exception, - controlsVisible, - controlsHidden, - setSpeed, - changedSubtitles, - changedTrack, - changedPlayerVisibility, - changedResolution -``` - -After creating BetterPlayerController you can add event listener this way: -```dart - _betterPlayerController.addEventsListener((event){ - print("Better player event: ${event.betterPlayerEventType}"); - }); -``` -Your event listener will ne auto-disposed on dispose time :) - - -### Change player behavior if player is not visible -You can change player behavior if player is not visible by using playerVisibilityChangedBehavior option in BetterPlayerConfiguration. -Here is an example for player used in list: -```dart - void onVisibilityChanged(double visibleFraction) async { - bool isPlaying = await _betterPlayerController.isPlaying(); - bool initialized = _betterPlayerController.isVideoInitialized(); - if (visibleFraction >= widget.playFraction) { - if (widget.autoPlay && initialized && !isPlaying && !_isDisposing) { - _betterPlayerController.play(); - } - } else { - if (widget.autoPause && initialized && isPlaying && !_isDisposing) { - _betterPlayerController.pause(); - } - } - } -``` -Player behavior works in the basis of VisibilityDetector (it uses visibilityFraction, which is value from 0.0 to 1.0 that describes how much given widget is on the viewport). So if value 0.0, player is not visible, so we need to pause the video. If the visibilityFraction is 1.0, we need to play it again. - -### Pass multiple resolutions of the video -You can setup video with different resolutions. Use resolutions parameter in data source. This should be used -only for normal videos (non-hls) to setup different qualities of the original video. - -```dart - var dataSource = BetterPlayerDataSource(BetterPlayerDataSourceType.network, - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", - resolutions: { - "LOW": - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", - "MEDIUM": - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_640_3MG.mp4", - "LARGE": - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", - "EXTRA_LARGE": - "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1920_18MG.mp4" - }); -``` -### Setup player notification - - - - - -
- - - - -
- -To setup player notification use notificationConfiguration in BetterPlayerDataSource. - -```dart -BetterPlayerDataSource dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - Constants.elephantDreamVideoUrl, - notificationConfiguration: BetterPlayerNotificationConfiguration( - showNotification: true, - title: "Elephant dream", - author: "Some author", - imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/African_Bush_Elephant.jpg/1200px-African_Bush_Elephant.jpg", - activityName: "MainActivity", - ), - ); -``` - -There are 3 majors parameters here: -title - name of the resource, shown in first line -author - author of the resource, shown in second line -imageUrl - image of the resource (optional). Can be both link to external image or internal file -activityName - name of activity used to open application back on notification click; used only for Activity - -If showNotification is set as true and no title and author is provided, then empty notification will be -displayed. - -User can control the player with notification buttons (i.e. play/pause, seek). When notification feature -is used when there are more players at the same time, then last player will be used. Notification will -be shown after play for the first time. - -To play resource after leaving the app, set handleLifecycle as false in your BetterPlayerConfiguration. - -Important note for android: -You need to add special service in android native code. Service will simply destroy all remaining notifications. -This service need to be used to handle situation when app is killed without proper player destroying. -Check BetterPlayerService in example project to see how to add this service to your app. -https://github.com/jhomlala/betterplayer/blob/feature/player_notifications/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt - -Here is an example of player with notification: https://github.com/jhomlala/betterplayer/blob/feature/player_notifications/example/lib/pages/notification_player_page.dart - - -### Add custom element to overflow menu -You can use BetterPlayerControlsConfiguration to add custom element to the overflow menu: -```dart - controlsConfiguration: BetterPlayerControlsConfiguration( - overflowMenuCustomItems: [ - BetterPlayerOverflowMenuItem( - Icons.account_circle_rounded, - "Custom element", - () => print("Click!"), - ) - ], - ), -``` - -### Enable/disable controls (always hidden if false) -```dart - betterPlayerController.setControlsEnabled(false); -``` - -### Set overridden aspect ratio. If passed overriddenAspectRatio will be used instead of aspectRatio. -If null then aspectRatio from BetterPlayerConfiguration will be used. -```dart - betterPlayerController.setOverriddenAspectRatio(1.0); -``` - -### Overridden duration -If overridden duration is set then video player will play video until this duration. -```dart -BetterPlayerDataSource dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - Constants.elephantDreamVideoUrl, - ///Play only 10 seconds of this video. - overriddenDuration: Duration(seconds: 10), - ); -``` - -### (iOS only) Add into info.plist (to support fullscreen rotation): -```xml -UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - -``` - -### Picture in Picture -Picture in Picture is not supported on all devices. -iOS: iOS > 14.0 -Android: Android > 8.0, enough RAM, v2 android embedding - -Each OS provides method to check if given device supports PiP. If device doesn't support PiP, then -error will be printed in console. - -To show PiP mode call this method: - -```dart - _betterPlayerController.enablePictureInPicture(_betterPlayerKey); -``` -BetterPlayerKey is a key which is used in BetterPlayer widget: - -```dart - GlobalKey _betterPlayerKey = GlobalKey(); - ... - AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer( - controller: _betterPlayerController, - key: _betterPlayerKey, - ), - ), -``` - -To hide PiP mode call this method: -```dart - _betterPlayerController.disablePictureInPicture(); -``` - -PiP menu item is enabled as default in both Material and Cuperino controls. You can disable it with -BetterPlayerControlsConfiguration's variable: enablePip. You can change PiP control menu icon with -pipMenuIcon variable. - -Warning: -Both Android and iOS PiP versions are in very early stage. There can be bugs and small issues. Please -make sure that you've checked state of the PiP in Better Player before moving it to the production. - -Known limitations: -Android: When PiP is enabled, Better Player will open full screen mode to play video correctly. When -user disables PiP, Better Player will back to the previous settings and for a half of second your device -will have incorrect orientation. - -### Set controls always visible -```dart - betterPlayerController.setControlsAlwaysVisible(true); -``` - -### DRM -To configure DRM for your data source, use drmConfiguration parameter. -Supported DRMs: - -* Token based (authorization header): Android/iOS -* Widevine (licensue url + headers): Android - -Additional DRM types may be added in the future. - -Token based: -```dart - BetterPlayerDataSource dataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "url", - videoFormat: BetterPlayerVideoFormat.hls, - drmConfiguration: BetterPlayerDrmConfiguration( - drmType: BetterPlayerDrmType.token, - token: - "Bearer=token", - ), - ); -```` - -Widevine (license url based): -```dart - BetterPlayerDataSource _widevineDataSource = BetterPlayerDataSource( - BetterPlayerDataSourceType.network, - "url", - drmConfiguration: BetterPlayerDrmConfiguration( - drmType: BetterPlayerDrmType.widevine, - licenseUrl: - "licenseUrl", - headers: {"header": "value"} - ), - ); - _widevineController.setupDataSource(_widevineDataSource); - -``` -### Set mix audio with others -You can enable mix with audio with others app with method: -```dart -betterPlayerController.setMixWithOthers(true) -``` -Default value is false. - -### More documentation -https://pub.dev/documentation/better_player/latest/better_player/better_player-library.html - -### Cache -Clear all cached data: - -```dart -betterPlayerController.clearCache(); -``` -Start pre cache before playing video (android only): -```dart -betterPlayerController.preCache(_betterPlayerDataSource); -``` -Stop running pre cache (android only): -```dart -betterPlayerController.stopPreCache(_betterPlayerDataSource); -``` 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 bfc2f2b1e..499042ea9 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java @@ -772,13 +772,18 @@ void setAudioTrack(String name, Integer index) { } TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); boolean hasElementWithoutLabel = false; + boolean hasStrangeAudioTrack = false; for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { TrackGroup group = trackGroupArray.get(groupIndex); for (int groupElementIndex = 0; groupElementIndex < group.length; groupElementIndex++) { - String label = group.getFormat(groupElementIndex).label; - if (label == null) { + Format format = group.getFormat(groupElementIndex); + + if (format.label == null) { hasElementWithoutLabel = true; - break; + } + + if (format.id.equals("1/15")) { + hasStrangeAudioTrack = true; } } } @@ -787,16 +792,22 @@ void setAudioTrack(String name, Integer index) { TrackGroup group = trackGroupArray.get(groupIndex); for (int groupElementIndex = 0; groupElementIndex < group.length; groupElementIndex++) { String label = group.getFormat(groupElementIndex).label; + if (name.equals(label) && index == groupIndex) { setAudioTrack(rendererIndex, groupIndex, groupElementIndex); return; } + ///Fallback option - if (hasElementWithoutLabel && index == groupIndex) { + if (!hasStrangeAudioTrack && hasElementWithoutLabel && index == groupIndex) { + setAudioTrack(rendererIndex, groupIndex, groupElementIndex); + return; + } + ///Fallback option + if (hasStrangeAudioTrack && name.equals(label)) { setAudioTrack(rendererIndex, groupIndex, groupElementIndex); return; } - } } } diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 000000000..fda69c63d --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,15 @@ +![logo](media/logo.png) + +# Better Player +> Advanced video player for Flutter. + +[![pub package](https://img.shields.io/pub/v/better_player.svg)](https://pub.dartlang.org/packages/better_player) +[![pub package](https://img.shields.io/github/license/jhomlala/betterplayer.svg?style=flat)](https://github.com/jhomlala/betterplayer) +[![pub package](https://img.shields.io/badge/platform-flutter-blue.svg)](https://github.com/jhomlala/betterplayer) + +- Highly customizable +- Solves many typical use cases and it's easy to run +- Supports both Android and iOS + +[GitHub](https://github.com/jhomlala/betterplayer) +[Get Started](#gettingstarted) \ No newline at end of file diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 000000000..6838376a7 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,31 @@ + +* Introduction + * [Home](home.md) + * [Install](install.md) + * [General player usage](generalplayerusage.md) + * [Playlist player usage](playlistplayerusage.md) + * [List player usage](listplayerusage.md) +* Customization + * [General configuration](generalconfiguration.md) + * [Data source configuration](datasourceconfiguration.md) + * [Controls configuration](controlsconfiguration.md) + * [Subtitles configuration](subtitlesconfiguration.md) + * [Cache configuration](cacheconfiguration.md) + * [Buffering configuration](bufferingconfiguration.md) + * [Notification configuration](notificationconfiguration.md) + * [Picture in Picture configuration](pictureinpictureconfiguration.md) + * [DRM configuration](drmconfiguration.md) + * [Playlist configuration](playlistconfiguration.md) + * [Translations configuration](translationsconfiguration.md) +* Additional features + * [Events](events.md) + * [Player behavior on visibility change](playerbehavioronvisibilitychange.md) + * [Resolutions of video](resolutionsofvideo.md) + * [Custom element in overflow menu](customelementinoverflowmenu.md) + * [Enable/disable controls](enabledisablecontrols.md) + * [Overridden aspect ratio](overriddenaspectratio.md) + * [Overriden duration](overriddenduration.md) + * [Mix audio with others](mixaudiowithothers.md) + * [Manual dispose](manualdispose.md) + * [Source load](sourceload.md) + * [Multiple gesture detector](multiplegesturedetector.md) \ No newline at end of file diff --git a/docs/basicusage.md b/docs/basicusage.md new file mode 100644 index 000000000..fdb5a8785 --- /dev/null +++ b/docs/basicusage.md @@ -0,0 +1,69 @@ +## Example project +Check [Example project](https://github.com/jhomlala/betterplayer/tree/master/example) which shows how to use Better Player in different scenarios. + +### Basic usage +There are 2 basic methods which you can use to setup Better Player: +```dart +BetterPlayer.network(url, configuration) +BetterPlayer.file(url, configuration) +``` +There methods setup basic configuration for you and allows you to start using player in few seconds. +Here is an example: +```dart + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Example player"), + ), + body: AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer.network( + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + betterPlayerConfiguration: BetterPlayerConfiguration( + aspectRatio: 16 / 9, + ), + ), + ), + ); + } +``` +In this example, we're just showing video from url with aspect ratio = 16/9. +Better Player has many more configuration options which are described in next pages. + + +### Normal usage +When you want have more configuration options then you need to create `BetterPlayerDataSource` and `BetterPlayerController`. `BetterPlayerDataSource` describes +source of your video. With `BetterPlayerDataSource` you will provide all important informations like url of video, type of video, subtitles source and more. +`BetterPlayerController` is a Flutter convention to have a manager class to control instance of video widget. With `BetterPlayerController` you will be able to +change behavior of the video widget, for example start or stop video, change volume and more. + + +Create `BetterPlayerDataSource` and `BetterPlayerController`. You should do it in initState of your widget: +```dart +BetterPlayerController _betterPlayerController; + + @override + void initState() { + super.initState(); + BetterPlayerDataSource betterPlayerDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"); + _betterPlayerController = BetterPlayerController( + BetterPlayerConfiguration(), + betterPlayerDataSource: betterPlayerDataSource); + } +```` + +Create `BetterPlayer` widget wrapped in `AspectRatio` widget: +```dart + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer( + controller: _betterPlayerController, + ), + ); + } +``` \ No newline at end of file diff --git a/docs/bufferingconfiguration b/docs/bufferingconfiguration new file mode 100644 index 000000000..689432f56 --- /dev/null +++ b/docs/bufferingconfiguration @@ -0,0 +1,22 @@ +### BetterPlayerBufferingConfiguration +Configuration class used to setup better buffering experience or setup custom load settings. Currently used only in Android. + +Possible configuration options: +```dart + ///The default minimum duration of media that the player will attempt to + ///ensure is buffered at all times, in milliseconds. + final int minBufferMs; + + ///The default maximum duration of media that the player will attempt to + ///buffer, in milliseconds. + final int maxBufferMs; + + ///The default duration of media that must be buffered for playback to start + ///or resume following a user action such as a seek, in milliseconds. + final int bufferForPlaybackMs; + + ///The default duration of media that must be buffered for playback to resume + ///after a rebuffer, in milliseconds. A rebuffer is defined to be caused by + ///buffer depletion rather than a user action. + final int bufferForPlaybackAfterRebufferMs; +``` \ No newline at end of file diff --git a/docs/bufferingconfiguration.md b/docs/bufferingconfiguration.md new file mode 100644 index 000000000..e3d53f2d5 --- /dev/null +++ b/docs/bufferingconfiguration.md @@ -0,0 +1,38 @@ +## Buffering configuration +Buffering of the video can be configurd with `BetterPlayerBufferingConfiguration` class. It allows to setup better buffering experience or setup custom load settings. Currently available only in Android. + + +`BetterPlayerBufferingConfiguration` should be used within `BetterPlayerDataSource`: + +```dart +BetterPlayerDataSource _betterPlayerDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.elephantDreamVideoUrl, + bufferingConfiguration: BetterPlayerBufferingConfiguration( + minBufferMs: 50000, + maxBufferMs: 13107200, + bufferForPlaybackMs: 2500, + bufferForPlaybackAfterRebufferMs: 5000, + ), + ); +``` + +Possible configuration options: +```dart +///The default minimum duration of media that the player will attempt to +///ensure is buffered at all times, in milliseconds. +final int minBufferMs; + +///The default maximum duration of media that the player will attempt to +///buffer, in milliseconds. +final int maxBufferMs; + +///The default duration of media that must be buffered for playback to start +///or resume following a user action such as a seek, in milliseconds. +final int bufferForPlaybackMs; + +///The default duration of media that must be buffered for playback to resume +///after a rebuffer, in milliseconds. A rebuffer is defined to be caused by +///buffer depletion rather than a user action. +final int bufferForPlaybackAfterRebufferMs; +``` \ No newline at end of file diff --git a/docs/cacheconfiguration.md b/docs/cacheconfiguration.md new file mode 100644 index 000000000..7ad688f86 --- /dev/null +++ b/docs/cacheconfiguration.md @@ -0,0 +1,51 @@ +## Cache configuration +Define cache configuration with `BetterPlayerCacheConfiguration` for given data source. Cache works only for network data sources. + +`BetterPlayerCacheConfiguration` should be used in `BetterPlayerDataSource`: + +```dart +BetterPlayerDataSource _betterPlayerDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.elephantDreamVideoUrl, + cacheConfiguration: BetterPlayerCacheConfiguration( + useCache: true, + preCacheSize: 10 * 1024 * 1024, + maxCacheSize: 10 * 1024 * 1024, + maxCacheFileSize: 10 * 1024 * 1024, + + ///Android only option to use cached video between app sessions + key: "testCacheKey", + ), + ); +``` + +```dart +///Enable cache for network data source +final bool useCache; + +/// The maximum cache size to keep on disk in bytes. +/// Android only option. +final int maxCacheSize; + +/// The maximum size of each individual file in bytes. +/// Android only option. +final int maxCacheFileSize; + +///Cache key to re-use same cached data between app sessions. +final String? key; +``` + +Clear all cached data: +```dart +betterPlayerController.clearCache(); +``` + +Start pre cache before playing video (android only): +```dart +betterPlayerController.preCache(_betterPlayerDataSource); +``` + +Stop running pre cache (android only): +```dart +betterPlayerController.stopPreCache(_betterPlayerDataSource); +``` diff --git a/docs/controlsconfiguration.md b/docs/controlsconfiguration.md new file mode 100644 index 000000000..bff62364d --- /dev/null +++ b/docs/controlsconfiguration.md @@ -0,0 +1,160 @@ +## Controls configuration +Controls (UI) of the player can be customized with `BetterPlayerControlsConfiguration` class. You should pass this configuration to `BetterPlayerConfiguration` instance. + +```dart +var betterPlayerConfiguration = BetterPlayerConfiguration( + controlsConfiguration: BetterPlayerControlsConfiguration( + textColor: Colors.black, + iconsColor: Colors.black, + ), +); +``` + + +```dart +///Color of the control bars +final Color controlBarColor; + +///Color of texts +final Color textColor; + +///Color of icons +final Color iconsColor; + +///Icon of play +final IconData playIcon; + +///Icon of pause +final IconData pauseIcon; + +///Icon of mute +final IconData muteIcon; + +///Icon of unmute +final IconData unMuteIcon; + +///Icon of fullscreen mode enable +final IconData fullscreenEnableIcon; + +///Icon of fullscreen mode disable +final IconData fullscreenDisableIcon; + +///Cupertino only icon, icon of skip +final IconData skipBackIcon; + +///Cupertino only icon, icon of forward +final IconData skipForwardIcon; + +///Flag used to enable/disable fullscreen +final bool enableFullscreen; + +///Flag used to enable/disable mute +final bool enableMute; + +///Flag used to enable/disable progress texts +final bool enableProgressText; + +///Flag used to enable/disable progress bar +final bool enableProgressBar; + +///Flag used to enable/disable progress bar drag +final bool enableProgressBarDrag; + +///Flag used to enable/disable play-pause +final bool enablePlayPause; + +///Flag used to enable skip forward and skip back +final bool enableSkips; + +///Progress bar played color +final Color progressBarPlayedColor; + +///Progress bar circle color +final Color progressBarHandleColor; + +///Progress bar buffered video color +final Color progressBarBufferedColor; + +///Progress bar background color +final Color progressBarBackgroundColor; + +///Time to hide controls +final Duration controlsHideTime; + +///Parameter used to build custom controls +final Widget Function(BetterPlayerController controller) + customControlsBuilder; + +///Parameter used to change theme of the player +final BetterPlayerTheme playerTheme; + +///Flag used to show/hide controls +final bool showControls; + +///Flag used to show controls on init +final bool showControlsOnInitialize; + +///Control bar height +final double controlBarHeight; + +///Live text color; +final Color liveTextColor; + +///Flag used to show/hide overflow menu which contains playback, subtitles, +///qualities options. +final bool enableOverflowMenu; + +///Flag used to show/hide playback speed +final bool enablePlaybackSpeed; + +///Flag used to show/hide subtitles +final bool enableSubtitles; + +///Flag used to show/hide qualities +final bool enableQualities; + +///Flag used to show/hide PiP mode +final bool enablePip; + +///Flag used to enable/disable retry feature +final bool enableRetry; + +///Flag used to show/hide audio tracks +final bool enableAudioTracks; + +///Custom items of overflow menu +final List overflowMenuCustomItems; + +///Icon of the overflow menu +final IconData overflowMenuIcon; + +///Icon of the playback speed menu item from overflow menu +final IconData playbackSpeedIcon; + +///Icon of the subtitles menu item from overflow menu +final IconData subtitlesIcon; + +///Icon of the qualities menu item from overflow menu +final IconData qualitiesIcon; + +///Icon of the audios menu item from overflow menu +final IconData audioTracksIcon; + +///Color of overflow menu icons +final Color overflowMenuIconsColor; + +///Time which will be used once user uses forward +final int forwardSkipTimeInMilliseconds; + +///Time which will be used once user uses backward +final int backwardSkipTimeInMilliseconds; + +///Color of default loading indicator +final Color loadingColor; + +///Widget which can be used instead of default progress +final Widget loadingWidget; + +///Color of the background, when no frame is displayed. +final Color backgroundColor; +``` \ No newline at end of file diff --git a/docs/customelementinoverflowmenu.md b/docs/customelementinoverflowmenu.md new file mode 100644 index 000000000..f1fbfe582 --- /dev/null +++ b/docs/customelementinoverflowmenu.md @@ -0,0 +1,14 @@ +## Custom element in overflow menu +You can use `BetterPlayerControlsConfiguration` to add custom element to the overflow menu: + +```dart +controlsConfiguration: BetterPlayerControlsConfiguration( + overflowMenuCustomItems: [ + BetterPlayerOverflowMenuItem( + Icons.account_circle_rounded, + "Custom element", + () => print("Click!"), + ) + ], + ), +``` \ No newline at end of file diff --git a/docs/datasourceconfiguration.md b/docs/datasourceconfiguration.md new file mode 100644 index 000000000..4d81085ad --- /dev/null +++ b/docs/datasourceconfiguration.md @@ -0,0 +1,86 @@ +## Data source configuration +Define source for one video in your app with `BetterPlayerDataSource`. + +There are 3 types of data sources: +* Network - data source which uses url to play video from external resources +* File - data source which uses url to play video from internal resources +* Memory - data source which uses list of bytes to play video from memory +```dart + var dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + subtitles: BetterPlayerSubtitlesSource( + type: BetterPlayerSubtitlesSourceType.file, + url: "${directory.path}/example_subtitles.srt", + ), + headers: {"header":"my_custom_header"} + ); +``` + +You can use type specific factories to build your data source. +Use `BetterPlayerDataSource.network` to build network data source, `BetterPlayerDataSource.file` to build file data source and `BetterPlayerDataSource.memory` to build memory data source. + +Possible configuration options: +```dart +///Type of source of video +final BetterPlayerDataSourceType type; + +///Url of the video +final String url; + +///Subtitles configuration +///You can pass here multiple subtitles +final List subtitles; + +///Flag to determine if current data source is live stream +final bool liveStream; + +/// Custom headers for player +final Map headers; + +///Should player use hls / dash subtitles (ASMS - Adaptive Streaming Media Sources). +final bool useAsmsSubtitles; + +///Should player use hls / dash tracks +final bool useAsmsTracks; + +///Should player use hls / dash audio tracks +final bool useAsmsAudioTracks; + +///List of strings that represents tracks names. +///If empty, then better player will choose name based on track parameters +final List hlsTrackNames; + +///Optional, alternative resolutions for non-hls video. Used to setup +///different qualities for video. +///Data should be in given format: +///{"360p": "url", "540p": "url2" } +final Map resolutions; + +///Optional cache configuration, used only for network data sources +final BetterPlayerCacheConfiguration cacheConfiguration; + +///List of bytes, used only in memory player +final List bytes; + +///Configuration of remote controls notification +final BetterPlayerNotificationConfiguration notificationConfiguration; + +///Duration which will be returned instead of original duration +final Duration overriddenDuration; + +///Video format hint when data source url has not valid extension. +final BetterPlayerVideoFormat videoFormat; + +///Extension of video without dot. Used only in memory data source. +final String videoExtension; + +///Configuration of content protection +final BetterPlayerDrmConfiguration drmConfiguration; + +///Placeholder widget which will be shown until video load or play. This +///placeholder may be useful if you want to show placeholder before each video +///in playlist. Otherwise, you should use placeholder from +/// BetterPlayerConfiguration. +final Widget placeholder; +``` \ No newline at end of file diff --git a/docs/drmconfiguration.md b/docs/drmconfiguration.md new file mode 100644 index 000000000..b9946134b --- /dev/null +++ b/docs/drmconfiguration.md @@ -0,0 +1,47 @@ +## DRM configuration +To configure DRM for your data source, use drmConfiguration parameter. +Supported DRMs: + +* Token based (authorization header): Android/iOS +* Widevine (licensue url + headers): Android +* Fairplay EZDRM (certificate url): iOS + +Additional DRM types may be added in the future. + +Token based: +```dart +BetterPlayerDataSource dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "url", + videoFormat: BetterPlayerVideoFormat.hls, + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.token, + token: "Bearer=token", + ), +); +```` + +Widevine (license url based): +```dart +BetterPlayerDataSource _widevineDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "url", + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.widevine, + licenseUrl:"licenseUrl", + headers: {"header": "value"} + ), +); +``` +Fairplay: + +```dart +BetterPlayerDataSource _fairplayDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.fairplayHlsUrl, + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.fairplay, + certificateUrl: Constants.fairplayCertificateUrl, + ), +); +``` \ No newline at end of file diff --git a/docs/enabledisablecontrols.md b/docs/enabledisablecontrols.md new file mode 100644 index 000000000..4c16683a5 --- /dev/null +++ b/docs/enabledisablecontrols.md @@ -0,0 +1,12 @@ +## Enable/disable controls + +You can change controls behavior to be always enabled/disabled: + +```dart +betterPlayerController.setControlsEnabled(false); +``` + +Set controls always visible (controls won't fade out, will be always over video): +```dart +betterPlayerController.setControlsAlwaysVisible(true); +``` diff --git a/docs/events.md b/docs/events.md new file mode 100644 index 000000000..bdc2b856c --- /dev/null +++ b/docs/events.md @@ -0,0 +1,29 @@ +## Events +You can listen to video player events like: +```dart + initialized, + play, + pause, + seekTo, + openFullscreen, + hideFullscreen, + setVolume, + progress, + finished, + exception, + controlsVisible, + controlsHidden, + setSpeed, + changedSubtitles, + changedTrack, + changedPlayerVisibility, + changedResolution +``` + +After creating `BetterPlayerController` you can add event listener this way: +```dart +_betterPlayerController.addEventsListener((event){ + print("Better player event: ${event.betterPlayerEventType}"); +}); +``` +Your event listener will be removed on dispose time automatically. diff --git a/docs/generalconfiguration.md b/docs/generalconfiguration.md new file mode 100644 index 000000000..edddcc83f --- /dev/null +++ b/docs/generalconfiguration.md @@ -0,0 +1,111 @@ +## General configuration +To setup general options of Better Player you need to create `BetterPlayerConfiguration` instance. You will use this object during creation of `BetterPlayerController`. + +```dart +var betterPlayerConfiguration = BetterPlayerConfiguration( + autoPlay: true, + looping: true, + fullScreenByDefault: true, +); +``` + +Possible configuration options which you can find in `BetterPlayerConfiguration`: +```dart +/// Play the video as soon as it's displayed +final bool autoPlay; + +/// Start video at a certain position +final Duration startAt; + +/// Whether or not the video should loop +final bool looping; + +/// Weather or not to show the controls when initializing the widget. +final bool showControlsOnInitialize; + +/// When the video playback runs into an error, you can build a custom +/// error message. +final Widget Function(BuildContext context, String errorMessage) errorBuilder; + +/// The Aspect Ratio of the Video. Important to get the correct size of the +/// video! +/// +/// Will fallback to fitting within the space allowed. +final double aspectRatio; + +/// The placeholder is displayed underneath the Video before it is initialized +/// or played. +final Widget placeholder; + +/// 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; + +/// Defines if the player will start in fullscreen when play is pressed +final bool fullScreenByDefault; + +/// Defines if the player will sleep in fullscreen or not +final bool allowedScreenSleep; + +/// Defines aspect ratio which will be used in fullscreen +final double fullScreenAspectRatio; + +/// Defines the set of allowed device orientations on entering fullscreen +final List deviceOrientationsOnFullScreen; + +/// Defines the system overlays visible after exiting fullscreen +final List systemOverlaysAfterFullScreen; + +/// Defines the set of allowed device orientations after exiting fullscreen +final List deviceOrientationsAfterFullScreen; + +/// Defines a custom RoutePageBuilder for the fullscreen +final BetterPlayerRoutePageBuilder routePageBuilder; + +/// Defines a event listener where video player events will be send +final Function(BetterPlayerEvent) eventListener; + +///Defines subtitles configuration +final BetterPlayerSubtitlesConfiguration subtitlesConfiguration; + +///Defines controls configuration +final BetterPlayerControlsConfiguration controlsConfiguration; + +///Defines fit of the video, allows to fix video stretching, see possible +///values here: https://api.flutter.dev/flutter/painting/BoxFit-class.html +final BoxFit fit; + +///Defines rotation of the video in degrees. Default value is 0. Can be 0, 90, 180, 270. +///Angle will rotate only video box, controls will be in the same place. +final double rotation; + +///Defines function which will react on player visibility changed +final Function(double visibilityFraction) playerVisibilityChangedBehavior; + +///Defines translations used in player. If null, then default english translations +///will be used. +final List translations; + +///Defines if player should auto detect full screen device orientation based +///on aspect ratio of the video. If aspect ratio of the video is < 1 then +///video will played in full screen in portrait mode. If aspect ratio is >= 1 +///then video will be played horizontally. If this parameter is true, then +///[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; +``` \ No newline at end of file diff --git a/docs/generalplayerusage.md b/docs/generalplayerusage.md new file mode 100644 index 000000000..aabfdf548 --- /dev/null +++ b/docs/generalplayerusage.md @@ -0,0 +1,71 @@ +## General player usage + +### Basic usage +There are 2 basic methods which you can use to setup Better Player: +```dart +BetterPlayer.network(url, configuration) +BetterPlayer.file(url, configuration) +``` +There methods setup basic configuration for you and allows you to start using player in few seconds. +Here is an example: +```dart + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Example player"), + ), + body: AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer.network( + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + betterPlayerConfiguration: BetterPlayerConfiguration( + aspectRatio: 16 / 9, + ), + ), + ), + ); + } +``` +In this example, we're just showing video from url with aspect ratio = 16/9. +Better Player has many more configuration options which are described in next pages. + + +### Normal usage +When you want have more configuration options then you need to create `BetterPlayerDataSource` and `BetterPlayerController`. `BetterPlayerDataSource` describes +source of your video. With `BetterPlayerDataSource` you will provide all important informations like url of video, type of video, subtitles source and more. +`BetterPlayerController` is a Flutter convention to have a manager class to control instance of video widget. With `BetterPlayerController` you will be able to +change behavior of the video widget, for example start or stop video, change volume and more. + + +Create `BetterPlayerDataSource` and `BetterPlayerController`. You should do it in initState of your widget: +```dart +BetterPlayerController _betterPlayerController; + + @override + void initState() { + super.initState(); + BetterPlayerDataSource betterPlayerDataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"); + _betterPlayerController = BetterPlayerController( + BetterPlayerConfiguration(), + betterPlayerDataSource: betterPlayerDataSource); + } +```` + +Create `BetterPlayer` widget wrapped in `AspectRatio` widget: +```dart + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer( + controller: _betterPlayerController, + ), + ); + } +``` + +## Example project +Check [Example project](https://github.com/jhomlala/betterplayer/tree/master/example) which shows how to use Better Player in different scenarios. \ No newline at end of file diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md new file mode 100644 index 000000000..eaeb2d0ee --- /dev/null +++ b/docs/gettingstarted.md @@ -0,0 +1 @@ +Getting started \ No newline at end of file diff --git a/docs/home.md b/docs/home.md new file mode 100644 index 000000000..84c1a7ee8 --- /dev/null +++ b/docs/home.md @@ -0,0 +1,84 @@ +# Better Player +This plugin is based on [Chewie](https://github.com/brianegan/chewie). Chewie is awesome plugin and works well in many cases. Better Player is a continuation of ideas introduced in Chewie. Better player fix common bugs, adds more configuration options and solves typical use cases. + +**Features:** +✔️ Fixed common bugs +✔️ Added advanced configuration options +✔️ Refactored player controls +✔️ Playlist support +✔️ Video in ListView support +✔️ Subtitles support: (formats: SRT, WEBVTT with HTML tags support; subtitles from HLS; multiple subtitles for video) +✔️ HTTP Headers support +✔️ BoxFit of video support +✔️ Playback speed support +✔️ HLS support (track, subtitles (also segmented), audio track selection) +✔️ DASH support (track, subtitles, audio track selection) +✔️ Alternative resolution support +✔️ Cache support +✔️ Notifications support +✔️ Picture in Picture support +✔️ DRM support (token, Widevine, FairPlay EZDRM). +✔️ ... and much more! + +## Important information +This plugin development is in progress. You may encounter breaking changes each version. This plugin is developed part-time for free. If you need +some feature which is supported by other players available in pub dev, then feel free to create PR. All valuable contributions are welcome! + +## Screenshots + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + +
\ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..7b58cc88c --- /dev/null +++ b/docs/index.html @@ -0,0 +1,49 @@ + + + + + Better Player + + + + + + + + + +
Please wait...
+ + + + + + + + + diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..bdc097861 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,33 @@ +## Install + +1. Add this to your **pubspec.yaml** file: + +```yaml +dependencies: + better_player: ^0.0.71 +``` + +2. Install it + +```bash +$ flutter pub get +``` + +3. Import it + +```dart +import 'package:better_player/better_player.dart'; +``` + +4. (Optional) iOS configuration + +Add this into your `info.plist` file to support full screen rotation (Better Player will rotate screen to horizontal position when full screen is enabled): + +```xml +UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + +``` \ No newline at end of file diff --git a/docs/listplayerusage.md b/docs/listplayerusage.md new file mode 100644 index 000000000..940a6ddcd --- /dev/null +++ b/docs/listplayerusage.md @@ -0,0 +1,24 @@ +## List player usage + +`BetterPlayerListViewPlayer` is one of the Better Player which has special function: to help displaying videos in list. + +`BetterPlayerListViewPlayer` will auto play/pause video once video is visible on screen with `playFraction`. `playFraction` describes percent of video that must be visibile to play video. If playFraction is 0.8 then 80% of video height must be visible on screen to automatically play the video. + +```dart + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayerListVideoPlayer( + BetterPlayerDataSource( + BetterPlayerDataSourceType.network, videoListData.videoUrl), + key: Key(videoListData.hashCode.toString()), + playFraction: 0.8, + ), + ); + } +``` + +You can control `BetterPlayerListViewPlayer` with `BetterPlayerListViewPlayerController`. You need to pass `BetterPlayerListViewPlayerController` to `BetterPlayerListVideoPlayer`. See more in example app. + +`BetterPlayerListViewPlayer` is good solution if you know that your list will be not too long. If you know that your list of videos will be long then you need to recycle `BetterPlayerController` instances. This is required because each creation of `BetterPlayerController` requires a lot of resources of the device. You need to remember that there are some devices which allows to create 2-3 instances of `BetterPlayerController` due to low hardware specification. To handle problem like this, you should use **recycle/reusable** techniques, where you will create 2-3 instances of `BetterPlayerController` and simply reuse them in list cell. See reusable video list example here: https://github.com/jhomlala/betterplayer/tree/master/example/lib/pages/reusable_video_list \ No newline at end of file diff --git a/docs/manualdispose.md b/docs/manualdispose.md new file mode 100644 index 000000000..405a916b3 --- /dev/null +++ b/docs/manualdispose.md @@ -0,0 +1,18 @@ +## Manual dispose + +Better Player disposes automatically `BetterPlayerController` when `BetterPlayer` widget will be removed from widget tree (when `dispose` method of `BetterPlayer` widget will be called by Flutter framework). + +If you're seeing error: `A VideoPlaverController was used after being disposed`, this means that your `BetterPlayer` widget got diposed and also `BetterPlayerController` got disposed. If you're building complex UI, you may decide whether to dispose `BetterPlayerController` manually. To enable manual disposal you need to set `autoDispose` flag to false in `BetterPlayerConfiguration`: + +```dart +BetterPlayerConfiguration betterPlayerConfiguration = + BetterPlayerConfiguration( + autoDispose: false, + ); +``` + +Now, when your `BetterPlayer` widget got disposed, your `BetterPlayerController` will stay alive and you need to dispose it manually once you'll know that you don't need that anymore: + +```dart +betterPlayerController.dispose(); +``` \ No newline at end of file diff --git a/docs/media/logo.png b/docs/media/logo.png new file mode 100644 index 000000000..223c5aa45 Binary files /dev/null and b/docs/media/logo.png differ diff --git a/docs/media/style.css b/docs/media/style.css new file mode 100644 index 000000000..9c8ba640a --- /dev/null +++ b/docs/media/style.css @@ -0,0 +1,53 @@ + +body .app-nav { + color: #34495e !important; +} +.cover { + color: #34495e !important; +} + +.cover #better-player .anchor span { + color: #34495e !important; +} + +section.cover { + min-height: 100vh; + height: auto !important; + padding: 2em 0.5em; +} + +section.cover .cover-main { + z-index: 0; +} + +section.cover .cover-main > .buttons a { + border-radius: 2rem; + border: 1px solid var(--theme-color, #42b983); + box-sizing: border-box; + color: var(--theme-color, #42b983); + display: inline-block; + font-size: 1.05rem; + letter-spacing: 0.1rem; + margin: 0.5rem 1rem; + padding: 0.75em 2rem; + text-decoration: none; + transition: all 0.15s ease; +} +section.cover .cover-main > .buttons a:last-child { + background-color: var(--theme-color, #42b983); + color: #fff; +} +section.cover .cover-main > .buttons a:last-child:hover { + color: inherit; + opacity: 0.8; +} +section.cover .cover-main > .buttons a:hover { + color: inherit; +} +section.cover blockquote > .buttons > a { + border-bottom: 2px solid var(--theme-color, #42b983); + transition: color 0.3s; +} +section.cover blockquote > .buttons > a:hover { + color: var(--theme-color, #42b983); +} \ No newline at end of file diff --git a/docs/mixaudiowithothers.md b/docs/mixaudiowithothers.md new file mode 100644 index 000000000..aab1d2b1e --- /dev/null +++ b/docs/mixaudiowithothers.md @@ -0,0 +1,6 @@ +### Mix audio with others +You can enable mix with audio with others app with method: +```dart +betterPlayerController.setMixWithOthers(true) +``` +Default value is false. \ No newline at end of file diff --git a/docs/multiplegesturedetector.md b/docs/multiplegesturedetector.md new file mode 100644 index 000000000..fea3f203b --- /dev/null +++ b/docs/multiplegesturedetector.md @@ -0,0 +1,22 @@ +## Multiple gesture detector + +If you need to wrap `BetterPlayer` widget with `GestureDetector` widget then you need to use `BetterPlayerMultipleGestureDetector`. + +```dart +BetterPlayerMultipleGestureDetector( + child: AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _betterPlayerController), + ), + onTap: () { + print("Tap!"); + }, + ), +``` + +Supported gestures: +* `onTap` +* `onDoubleTap` +* `onLongPress` + +If you need to have different gestures than mentioned above then you can use default `GestureDetector`. \ No newline at end of file diff --git a/docs/notificationconfiguration.md b/docs/notificationconfiguration.md new file mode 100644 index 000000000..dfbcf2cb0 --- /dev/null +++ b/docs/notificationconfiguration.md @@ -0,0 +1,51 @@ +## Notification configuration + + + + + +
+ + + + +
+ +To setup player notification use `notificationConfiguration` parameter in `BetterPlayerDataSource`. + +```dart +BetterPlayerDataSource dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.elephantDreamVideoUrl, + notificationConfiguration: BetterPlayerNotificationConfiguration( + showNotification: true, + title: "Elephant dream", + author: "Some author", + imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/African_Bush_Elephant.jpg/1200px-African_Bush_Elephant.jpg", + activityName: "MainActivity", + ), + ); +``` + +There are 3 majors parameters here: +`title` - name of the resource, shown in first line; +`author` - author of the resource, shown in second line; +`imageUrl` - image of the resource (optional). Can be both link to external image or internal file; +`activityName` - name of activity used to open application back on notification click; used only for Activity; + +If `showNotification` is set as true and no title and author is provided, then empty notification will be +displayed. + +User can control the player with notification buttons (i.e. play/pause, seek). When notification feature +is used when there are more players at the same time, then last player will be used. Notification will +be shown after play for the first time. + +To play resource after leaving the app, set `handleLifecycle` as false in your `BetterPlayerConfiguration`. + +Important note for android: +You need to add special service in android native code. Service will simply destroy all remaining notifications. +This service need to be used to handle situation when app is killed without proper player destroying. +Check `BetterPlayerService` in example project to see how to add this service to your app. +https://github.com/jhomlala/betterplayer/blob/feature/player_notifications/example/android/app/src/main/kotlin/com/jhomlala/better_player_example/BetterPlayerService.kt + +Here is an example of player with notification: https://github.com/jhomlala/betterplayer/blob/feature/player_notifications/example/lib/pages/notification_player_page.dart \ No newline at end of file diff --git a/docs/overriddenaspectratio.md b/docs/overriddenaspectratio.md new file mode 100644 index 000000000..f8d630795 --- /dev/null +++ b/docs/overriddenaspectratio.md @@ -0,0 +1,6 @@ +## Overriden aspect ratio + +You can override `BetterPlayerConfiguration`'s `aspectRatio` parameter in runtime with `setOverridenAspectRatio` method from `betterPlayerController`. +```dart +betterPlayerController.setOverriddenAspectRatio(1.0); +``` \ No newline at end of file diff --git a/docs/overriddenduration.md b/docs/overriddenduration.md new file mode 100644 index 000000000..7c4b2ea3f --- /dev/null +++ b/docs/overriddenduration.md @@ -0,0 +1,11 @@ +## Overridden duration +If `overriddenDuration` is set then video player will play video until this duration. This feature can be used to cut long videos into smaller one. + +```dart +BetterPlayerDataSource dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.elephantDreamVideoUrl, + ///Play only 10 seconds of this video. + overriddenDuration: const Duration(seconds: 10), +); +``` \ No newline at end of file diff --git a/docs/pictureinpictureconfiguration.md b/docs/pictureinpictureconfiguration.md new file mode 100644 index 000000000..4b38bee46 --- /dev/null +++ b/docs/pictureinpictureconfiguration.md @@ -0,0 +1,51 @@ +## Picture in Picture configuration +Picture in Picture is not supported on all devices. + +Requirements: +* iOS: iOS version greater than 14.0 +* Android: Android version greater than 8.0, enough RAM, v2 Flutter android embedding + +Each OS provides method to check if given device supports PiP. If device doesn't support PiP, then +error will be printed in console. + +Check if PiP is supported in given device: +```dart +_betterPlayerController.isPictureInPictureSupported(); +``` + +To show PiP mode call this method: + +```dart +_betterPlayerController.enablePictureInPicture(_betterPlayerKey); +``` +`_betterPlayerKey` is a key which is used in BetterPlayer widget: + +```dart +GlobalKey _betterPlayerKey = GlobalKey(); +... + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer( + controller: _betterPlayerController, + key: _betterPlayerKey, + ), + ), +``` + +To hide PiP mode call this method: +```dart +betterPlayerController.disablePictureInPicture(); +``` + +PiP menu item is enabled as default in both Material and Cuperino controls. You can disable it with +`BetterPlayerControlsConfiguration`'s variable: `enablePip`. You can change PiP control menu icon with +`pipMenuIcon` variable in `BetterPlayerControlsConfiguration`. + +Warning: +Both Android and iOS PiP versions are in very early stage. There can be bugs and small issues. Please +make sure that you've checked state of the PiP in Better Player before moving it to the production. + +Known limitations: +Android: When PiP is enabled, Better Player will open full screen mode to play video correctly. When +user disables PiP, Better Player will back to the previous settings and for a half of second your device +will have incorrect orientation. \ No newline at end of file diff --git a/docs/playerbehavioronvisibilitychange.md b/docs/playerbehavioronvisibilitychange.md new file mode 100644 index 000000000..283f2671a --- /dev/null +++ b/docs/playerbehavioronvisibilitychange.md @@ -0,0 +1,21 @@ +## Player behavior on visibility change +You can change player behavior if player is not visible by using `playerVisibilityChangedBehavior` option from `BetterPlayerConfiguration`. +Here is an example for player used in list: + +```dart +void onVisibilityChanged(double visibleFraction) async { + bool isPlaying = await _betterPlayerController.isPlaying(); + bool initialized = _betterPlayerController.isVideoInitialized(); + if (visibleFraction >= widget.playFraction) { + if (widget.autoPlay && initialized && !isPlaying && !_isDisposing) { + _betterPlayerController.play(); + } + } else { + if (widget.autoPause && initialized && isPlaying && !_isDisposing) { + _betterPlayerController.pause(); + } + } +} +``` + +Player behavior works in the basis of `VisibilityDetector` (it uses `visibilityFraction`, which is value from 0.0 to 1.0 that describes how much given widget is on the viewport). So if value 0.0, player is not visible, so we need to pause the video. If the `visibilityFraction` is 1.0, we need to play it again. diff --git a/docs/playlistconfiguration.md b/docs/playlistconfiguration.md new file mode 100644 index 000000000..30ce47f0d --- /dev/null +++ b/docs/playlistconfiguration.md @@ -0,0 +1,24 @@ +## Playlist configuration + +Customize `BetterPlayerPlaylist` widget behavior with `BetterPlayerPlaylistConfiguration`. Instance of `BetterPlayerPlaylistConfiguration` should be passed to `BetterPlayerPlaylist`. + + +```dart +var betterPlayerPlaylistConfiguration = BetterPlayerPlaylistConfiguration( + loopVideos: false, + nextVideoDelay: Duration(milliseconds: 5000), +); +``` + +Possible configuration options: +```dart +///How long user should wait for next video +final Duration nextVideoDelay; + +///Should videos be looped +final bool loopVideos; + +///Index of video that will start on playlist start. Id must be less than +///elements in data source list. Default is 0. +final int initialStartIndex; +``` \ No newline at end of file diff --git a/docs/playlistplayerusage.md b/docs/playlistplayerusage.md new file mode 100644 index 000000000..1c48a2d3a --- /dev/null +++ b/docs/playlistplayerusage.md @@ -0,0 +1,40 @@ +## Playlist player usage +Playlist is a one of the Better Player widgets which allows to configure list of videos which will be played one after another. +Once video finishes, there's interval time (customizable) after which next video will be played. This interval time is displayed in +player UI and it's skippable. + +To use playlist, you need to create dataset with multiple videos: +```dart + List createDataSet() { + List dataSourceList = List(); + dataSourceList.add( + BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + ), + ); + dataSourceList.add( + BetterPlayerDataSource(BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"), + ); + dataSourceList.add( + BetterPlayerDataSource(BetterPlayerDataSourceType.network, + "http://sample.vodobox.com/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8"), + ); + return dataSourceList; + } +``` + +Then create `BetterPlayerPlaylist` widget: +```dart +@override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayerPlaylist( + betterPlayerConfiguration: BetterPlayerConfiguration(), + betterPlayerPlaylistConfiguration: const BetterPlayerPlaylistConfiguration(), + betterPlayerDataSourceList: dataSourceList), + ); + } +``` diff --git a/docs/resolutionsofvideo.md b/docs/resolutionsofvideo.md new file mode 100644 index 000000000..c52e3b4b9 --- /dev/null +++ b/docs/resolutionsofvideo.md @@ -0,0 +1,19 @@ + +## Resolutions of the video +You can setup video with different resolutions. Use resolutions parameter in data source. This should be used +only for normal videos (non-hls, non-dash) to setup different qualities of the original video. + +```dart +var dataSource = BetterPlayerDataSource(BetterPlayerDataSourceType.network, + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", + resolutions: { + "LOW": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4", + "MEDIUM": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_640_3MG.mp4", + "LARGE": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1280_10MG.mp4", + "EXTRA_LARGE": + "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_1920_18MG.mp4" + }); +``` \ No newline at end of file diff --git a/docs/sourceload.md b/docs/sourceload.md new file mode 100644 index 000000000..7ad9291c7 --- /dev/null +++ b/docs/sourceload.md @@ -0,0 +1,14 @@ +## Source load +You can check whether your data source has been loaded successfully by checking result of the future method of `setupDataSource` in `BetterPlayerController`: + +```dart +betterPlayerController!.setupDataSource(source) +.then((response) { + // Source loaded successfully + videoLoading = false; +}) +.catchError((error) async { + // Source did not load, url might be invalid + inspect(error); +}); +``` diff --git a/docs/subtitlesconfiguration.md b/docs/subtitlesconfiguration.md new file mode 100644 index 000000000..f117f3627 --- /dev/null +++ b/docs/subtitlesconfiguration.md @@ -0,0 +1,143 @@ +## Subtitles source +Subtitles can be configured from 3 different sources: file, network and memory. Subtitles source is passed in `BetterPlayerDataSource`: + +Network subtitles: +```dart +var dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", + subtitles: BetterPlayerSubtitlesSource.single( + type: BetterPlayerSubtitlesSourceType.network, + url: "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt" + ), +); +``` + +File subtitles: +```dart +var dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + "${directory.path}/testvideo.mp4", + subtitles: BetterPlayerSubtitlesSource.single( + type: BetterPlayerSubtitlesSourceType.file, + url: "${directory.path}/example_subtitles.srt", + ), +); +``` + +You can pass multiple subtitles for one video: + +```dart +var dataSource = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8", + liveStream: false, + useAsmsSubtitles: true, + hlsTrackNames: ["Low quality", "Not so low quality", "Medium quality"], + subtitles: [ + BetterPlayerSubtitlesSource( + type: BetterPlayerSubtitlesSourceType.network, + name: "EN", + urls: [ + "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt" + ], + ), + + BetterPlayerSubtitlesSource( + type: BetterPlayerSubtitlesSourceType.network, + name: "DE", + urls: [ + "https://dl.dropboxusercontent.com/s/71nzjo2ux3evxqk/example_subtitles.srt" + ], + ), + ], +); +``` + + +Possible `BetterPlayerSubtitlesSource` configuration options: +```dart +///Source type +final BetterPlayerSubtitlesSourceType? type; + +///Name of the subtitles, default value is "Default subtitles" +final String? name; + +///Url of the subtitles, used with file or network subtitles +final List? urls; + +///Content of subtitles, used when type is memory +final String? content; + +///Subtitles selected by default, without user interaction +final bool? selectedByDefault; + +///Additional headers used in HTTP request. Works only for +/// [BetterPlayerSubtitlesSourceType.memory] source type. +final Map? headers; + +///Is ASMS segmented source (more than 1 subtitle file). This shouldn't be +///configured manually. +final bool? asmsIsSegmented; + +///Max. time between segments in milliseconds. This shouldn't be configured +/// manually. +final int? asmsSegmentsTime; + +///List of segments (start,end,url of the segment). This shouldn't be +///configured manually. +final List? asmsSegments; +``` + + +## Subtitles configuration + +Subtitles can be configured with `BetterPlayerSubtitlesConfiguration` class. Instance of this configuration should be passed to `BetterPlayerConfiguration`. + +```dart +var betterPlayerConfiguration = BetterPlayerConfiguration( + subtitlesConfiguration: BetterPlayerSubtitlesConfiguration( + fontSize: 20, + fontColor: Colors.green, + ), +); +``` + +Possible configuration options: +```dart +///Subtitle font size +final double fontSize; + +///Subtitle font color +final Color fontColor; + +///Enable outline (border) of the text +final bool outlineEnabled; + +///Color of the outline stroke +final Color outlineColor; + +///Outline stroke size +final double outlineSize; + +///Font family of the subtitle +final String fontFamily; + +///Left padding of the subtitle +final double leftPadding; + +///Right padding of the subtitle +final double rightPadding; + +///Bottom padding of the subtitle +final double bottomPadding; + +///Alignment of the subtitle +final Alignment alignment; + +///Background color of the subtitle +final Color backgroundColor; + +///Subtitles selected by default, without user interaction +final bool selectedByDefault; +``` \ No newline at end of file diff --git a/docs/translationsconfiguration.md b/docs/translationsconfiguration.md new file mode 100644 index 000000000..be16ac710 --- /dev/null +++ b/docs/translationsconfiguration.md @@ -0,0 +1,36 @@ +## Translations configuration +You can provide translations for different languages with `BetterPlayerTranslations` class. You need to pass list of `BetterPlayerTranslations` to the `BetterPlayerConfiguration`. Here is an example: + +```dart + translations: [ + BetterPlayerTranslations( + languageCode: "language_code for example pl", + generalDefaultError: "translated text", + generalNone: "translated text", + generalDefault: "translated text", + playlistLoadingNextVideo: "translated text", + controlsLive: "translated text", + controlsNextVideoIn: "translated text", + overflowMenuPlaybackSpeed: "translated text", + overflowMenuSubtitles: "translated text", + overflowMenuQuality: "translated text", + ), + BetterPlayerTranslations( + languageCode: "other language for example cz", + generalDefaultError: "translated text", + generalNone: "translated text", + generalDefault: "translated text", + playlistLoadingNextVideo: "translated text", + controlsLive: "translated text", + controlsNextVideoIn: "translated text", + overflowMenuPlaybackSpeed: "translated text", + overflowMenuSubtitles: "translated text", + overflowMenuQuality: "translated text", + ), + ], +``` +There are 8 pre build in languages: EN, PL, ZH (chinese simplified), HI (hindi), AR, TR, VI, ES. If you didn't provide +any translation then EN translations will be used or any of the pre build in translations, only if it's match current user locale. + +You need to setup localizations in your app first to make it work. Here's how you can do that: +https://flutter.dev/docs/development/accessibility-and-localization/internationalization \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index c3e85958c..985506b44 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:better_player_example/pages/welcome_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; void main() => runApp(MyApp()); @@ -7,21 +8,25 @@ void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Better player demo', - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: [ - const Locale('en', 'US'), - const Locale('pl', 'PL'), - ], - theme: ThemeData( - primarySwatch: Colors.green, - accentColor: Colors.green, - ), - home: WelcomePage(), - ); + return Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(), + }, + child: MaterialApp( + title: 'Better player demo', + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', 'US'), + const Locale('pl', 'PL'), + ], + theme: ThemeData( + primarySwatch: Colors.green, + accentColor: Colors.green, + ), + home: WelcomePage(), + )); } } diff --git a/example/lib/pages/normal_player_page.dart b/example/lib/pages/normal_player_page.dart index 06e6540bd..99df9acfa 100644 --- a/example/lib/pages/normal_player_page.dart +++ b/example/lib/pages/normal_player_page.dart @@ -37,9 +37,14 @@ class _NormalPlayerPageState extends State { body: Column( children: [ const SizedBox(height: 8), - AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer(controller: _betterPlayerController), + BetterPlayerMultipleGestureDetector( + child: AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _betterPlayerController), + ), + onTap: () { + print("Tap!"); + }, ), ], ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5a57d05df..0f3a977d4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,9 +14,9 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - path_provider: ^2.0.1 - visibility_detector: ^0.2.0-nullsafety.1 - collection: ^1.15.0-nullsafety.4 + path_provider: ^2.0.2 + visibility_detector: ^0.2.0 + collection: ^1.15.0 dev_dependencies: flutter_test: diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 000000000..e69de29bb diff --git a/flutter_02.png b/flutter_02.png new file mode 100644 index 000000000..ff602c127 Binary files /dev/null and b/flutter_02.png differ diff --git a/ios/Classes/BetterPlayer.m b/ios/Classes/BetterPlayer.m index 03fcabfb9..5625f7255 100644 --- a/ios/Classes/BetterPlayer.m +++ b/ios/Classes/BetterPlayer.m @@ -522,11 +522,9 @@ - (void)seekTo:(int)location { toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished){ if (wasPlaying){ - [self->_player play]; + _player.rate = _playerRate; } - _player.rate = _playerRate; }]; - } - (void)setIsLooping:(bool)isLooping { @@ -690,12 +688,12 @@ - (void) setAudioTrack:(NSString*) name index:(int) index{ NSArray* options = audioSelectionGroup.options; - for (int index = 0; index < [options count]; index++) { - AVMediaSelectionOption* option = [options objectAtIndex:index]; + for (int audioTrackIndex = 0; audioTrackIndex < [options count]; audioTrackIndex++) { + AVMediaSelectionOption* option = [options objectAtIndex:audioTrackIndex]; NSArray *metaDatas = [AVMetadataItem metadataItemsFromArray:option.commonMetadata withKey:@"title" keySpace:@"comn"]; if (metaDatas.count > 0) { NSString *title = ((AVMetadataItem*)[metaDatas objectAtIndex:0]).stringValue; - if (title == name && index == index ){ + if ([name compare:title] == NSOrderedSame && audioTrackIndex == index ){ [[_player currentItem] selectMediaOption:option inMediaSelectionGroup: audioSelectionGroup]; } } diff --git a/lib/better_player.dart b/lib/better_player.dart index 0a2c58f91..856ef890c 100644 --- a/lib/better_player.dart +++ b/lib/better_player.dart @@ -6,6 +6,7 @@ export 'src/asms/better_player_asms_subtitle.dart'; export 'src/asms/better_player_asms_subtitle_segment.dart'; export 'src/asms/better_player_asms_track.dart'; export 'src/asms/better_player_asms_utils.dart'; +export 'src/configuration/better_player_buffering_configuration.dart'; export 'src/configuration/better_player_cache_configuration.dart'; export 'src/configuration/better_player_configuration.dart'; export 'src/configuration/better_player_controls_configuration.dart'; @@ -19,8 +20,8 @@ export 'src/configuration/better_player_notification_configuration.dart'; export 'src/configuration/better_player_theme.dart'; export 'src/configuration/better_player_translations.dart'; export 'src/configuration/better_player_video_format.dart'; -export 'src/configuration/better_player_buffering_configuration.dart'; export 'src/controls/better_player_controls_state.dart'; +export 'src/controls/better_player_multiple_gesture_detector.dart'; export 'src/controls/better_player_overflow_menu_item.dart'; export 'src/controls/better_player_progress_colors.dart'; export 'src/core/better_player.dart'; diff --git a/lib/src/configuration/better_player_buffering_configuration.dart b/lib/src/configuration/better_player_buffering_configuration.dart index 006eb7358..edc36aef3 100644 --- a/lib/src/configuration/better_player_buffering_configuration.dart +++ b/lib/src/configuration/better_player_buffering_configuration.dart @@ -3,10 +3,10 @@ class BetterPlayerBufferingConfiguration { ///Constants values are from the offical exoplayer documentation ///https://exoplayer.dev/doc/reference/constant-values.html#com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS - static const DEFAULT_MIN_BUFFER_MS = 50000; - static const DEFAULT_MAX_BUFFER_MS = 13107200; - static const DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500; - static const DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + static const defaultMinBufferMs = 50000; + static const defaultMaxBufferMs = 13107200; + static const defaultBufferForPlaybackMs = 2500; + static const defaultBufferForPlaybackAfterRebufferMs = 5000; /// The default minimum duration of media that the player will attempt to /// ensure is buffered at all times, in milliseconds. @@ -26,10 +26,10 @@ class BetterPlayerBufferingConfiguration { final int bufferForPlaybackAfterRebufferMs; const BetterPlayerBufferingConfiguration({ - this.minBufferMs = DEFAULT_MIN_BUFFER_MS, - this.maxBufferMs = DEFAULT_MAX_BUFFER_MS, - this.bufferForPlaybackMs = DEFAULT_BUFFER_FOR_PLAYBACK_MS, + this.minBufferMs = defaultMinBufferMs, + this.maxBufferMs = defaultMaxBufferMs, + this.bufferForPlaybackMs = defaultBufferForPlaybackMs, this.bufferForPlaybackAfterRebufferMs = - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + defaultBufferForPlaybackAfterRebufferMs, }); } diff --git a/lib/src/controls/better_player_cupertino_controls.dart b/lib/src/controls/better_player_cupertino_controls.dart index dac518a99..78fe5c8de 100644 --- a/lib/src/controls/better_player_cupertino_controls.dart +++ b/lib/src/controls/better_player_cupertino_controls.dart @@ -4,6 +4,7 @@ import 'dart:ui' as ui; // Flutter imports: import 'package:better_player/src/configuration/better_player_controls_configuration.dart'; +import 'package:better_player/src/controls/better_player_multiple_gesture_detector.dart'; import 'package:flutter/material.dart'; // Project imports: @@ -84,6 +85,9 @@ class _BetterPlayerCupertinoControlsState _wasLoading = isLoading(_latestValue); return GestureDetector( onTap: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onTap?.call(); + } _hideStuff ? cancelAndRestartTimer() : setState(() { @@ -91,9 +95,17 @@ class _BetterPlayerCupertinoControlsState }); }, onDoubleTap: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onDoubleTap?.call(); + } cancelAndRestartTimer(); _onPlayPause(); }, + onLongPress: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onLongPress?.call(); + } + }, child: AbsorbPointer( absorbing: _hideStuff, child: Column( diff --git a/lib/src/controls/better_player_material_controls.dart b/lib/src/controls/better_player_material_controls.dart index 1ed1144c8..2f6a5b76a 100644 --- a/lib/src/controls/better_player_material_controls.dart +++ b/lib/src/controls/better_player_material_controls.dart @@ -6,6 +6,7 @@ import 'package:better_player/src/configuration/better_player_controls_configura import 'package:better_player/src/controls/better_player_clickable_widget.dart'; import 'package:better_player/src/controls/better_player_controls_state.dart'; import 'package:better_player/src/controls/better_player_material_progress_bar.dart'; +import 'package:better_player/src/controls/better_player_multiple_gesture_detector.dart'; import 'package:better_player/src/controls/better_player_progress_colors.dart'; import 'package:better_player/src/core/better_player_controller.dart'; import 'package:better_player/src/core/better_player_utils.dart'; @@ -14,8 +15,6 @@ import 'package:better_player/src/video_player/video_player.dart'; // Flutter imports: import 'package:flutter/material.dart'; -import 'better_player_clickable_widget.dart'; - class BetterPlayerMaterialControls extends StatefulWidget { ///Callback used to send information if player bar is hidden or not final Function(bool visbility) onControlsVisibilityChanged; @@ -73,6 +72,9 @@ class _BetterPlayerMaterialControlsState } return GestureDetector( onTap: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onTap?.call(); + } _hideStuff ? cancelAndRestartTimer() : setState(() { @@ -80,9 +82,17 @@ class _BetterPlayerMaterialControlsState }); }, onDoubleTap: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onDoubleTap?.call(); + } cancelAndRestartTimer(); _onPlayPause(); }, + onLongPress: () { + if (BetterPlayerMultipleGestureDetector.of(context) != null) { + BetterPlayerMultipleGestureDetector.of(context)!.onLongPress?.call(); + } + }, child: AbsorbPointer( absorbing: _hideStuff, child: Column( diff --git a/lib/src/controls/better_player_multiple_gesture_detector.dart b/lib/src/controls/better_player_multiple_gesture_detector.dart new file mode 100644 index 000000000..c43f00e4a --- /dev/null +++ b/lib/src/controls/better_player_multiple_gesture_detector.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +///Helper class for GestureDetector used within Better Player. Used to pass +///gestures to upper GestureDetectors. +class BetterPlayerMultipleGestureDetector extends InheritedWidget { + final void Function()? onTap; + final void Function()? onDoubleTap; + final void Function()? onLongPress; + + const BetterPlayerMultipleGestureDetector({ + Key? key, + required Widget child, + this.onTap, + this.onDoubleTap, + this.onLongPress, + }) : super(key: key, child: child); + + static BetterPlayerMultipleGestureDetector? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType< + BetterPlayerMultipleGestureDetector>(); + } + + @override + bool updateShouldNotify(BetterPlayerMultipleGestureDetector oldWidget) => + false; +} diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index fec3c21b3..012b6da0b 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -61,6 +61,9 @@ class BetterPlayerController { ///between flutter high level code and lower level native code. VideoPlayerController? videoPlayerController; + ///Expose all active eventListeners + List get eventListeners => _eventListeners; + /// Defines a event listener where video player events will be send. Function(BetterPlayerEvent)? get eventListener => betterPlayerConfiguration.eventListener; @@ -470,7 +473,8 @@ class BetterPlayerController { case BetterPlayerDataSourceType.file: final file = File(betterPlayerDataSource.url); if (!file.existsSync()) { - print("File ${file.path} doesn't exists. This may be because " + BetterPlayerUtils.log( + "File ${file.path} doesn't exists. This may be because " "you're acessing file from native path and Flutter doesn't " "recognize this path."); } diff --git a/lib/src/video_player/video_player.dart b/lib/src/video_player/video_player.dart index c6619696a..866546fe8 100644 --- a/lib/src/video_player/video_player.dart +++ b/lib/src/video_player/video_player.dart @@ -532,8 +532,8 @@ class VideoPlayerController extends ValueNotifier { /// and silently clamped. Future seekTo(Duration? position) async { bool isPlaying = value.isPlaying; - int positionInMs = value.position.inMilliseconds; - int durationInMs = value.duration?.inMilliseconds ?? 0; + final int positionInMs = value.position.inMilliseconds; + final int durationInMs = value.duration?.inMilliseconds ?? 0; if (positionInMs >= durationInMs && position?.inMilliseconds == 0) { isPlaying = true; @@ -573,8 +573,14 @@ class VideoPlayerController extends ValueNotifier { /// /// [speed] indicates a value between 0.0 and 2.0 on a linear scale. Future setSpeed(double speed) async { - value = value.copyWith(speed: speed); - await _applySpeed(); + final double previousSpeed = value.speed; + try { + value = value.copyWith(speed: speed); + await _applySpeed(); + } catch (exception) { + value = value.copyWith(speed: previousSpeed); + rethrow; + } } /// Sets the video track parameters of [this] diff --git a/pubspec.lock b/pubspec.lock index 9ae708522..45f28455e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -56,7 +56,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" fake_async: dependency: transitive description: @@ -160,7 +160,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -195,7 +195,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.11.1" petitparser: dependency: transitive description: @@ -347,7 +347,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.1.1" + version: "5.1.2" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index bfc58d88b..7fe08ff28 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.70 +version: 0.0.71 authors: - Jakub Homlala homepage: https://github.com/jhomlala/betterplayer @@ -12,15 +12,15 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.3 wakelock: ^0.5.2 - pedantic: ^1.11.0 + pedantic: ^1.11.1 meta: ^1.3.0 flutter_widget_from_html_core: ^0.6.1+1 visibility_detector: ^0.2.0 - path_provider: ^2.0.1 + path_provider: ^2.0.2 collection: ^1.15.0 - xml: ^5.1.1 + xml: ^5.1.2 dev_dependencies: flutter_test: @@ -36,4 +36,4 @@ flutter: package: com.jhomlala.better_player pluginClass: BetterPlayerPlugin ios: - pluginClass: BetterPlayerPlugin + pluginClass: BetterPlayerPlugin \ No newline at end of file