diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f29b4fa..459cdc347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 0.0.73 +* Added `licenseUrl` support for iOS DRM. +* Fixed RTL text direction issue in player controls. +* Added `renderedSubtitle` in `BetterPlayerController`. +* Added additional check in `postControllerEvent` to handle scenario where event stream is closed. +* Updated ExoPlayer version +* Fixed `bufferingUpdate` event triggered too often. +* Updated video list example with bufering configuration. +* Updated video list documentation. +* Added `setMixWithOthers` method in `BetterPlayerListVideoPlayerController`. +* Fixed broken link in cover page of documentation. +* Fixed progress bar issue where position could be set above video duration. +* Fixed iOS remote notification command issue. +* Removed duplicated page in example app (by https://github.com/pinguluk) +* Added support for clear key DRM (by https://github.com/tinusneethling) +* Refreshed look and feel of the player UI (by https://github.com/creativeblaq) +* Added `sigmaX` and `sigmaY` parameters in BetterPlayerControlsConfiguration to control blur of cupertino controls (original idea by: https://github.com/YeFei572) + ## 0.0.72 * Updated ExoPlayer version diff --git a/analysis_options.yaml b/analysis_options.yaml index 30086d5fb..5fc901eea 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,4 +17,5 @@ linter: sized_box_for_whitespace: false invalid_dependency: false sort_pub_dependencies: false + avoid_unnecessary_containers: false diff --git a/android/build.gradle b/android/build.gradle index b68cad2f7..208c538da 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,12 +39,12 @@ android { } dependencies { - implementation 'com.google.android.exoplayer:exoplayer-core:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.1' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.1' - implementation 'com.google.android.exoplayer:extension-mediasession:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-core:2.14.2' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.2' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.2' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.2' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.14.2' + implementation 'com.google.android.exoplayer:extension-mediasession:2.14.2' implementation "android.arch.lifecycle:runtime:1.1.1" implementation "android.arch.lifecycle:common:1.1.1" implementation "android.arch.lifecycle:common-java8:1.1.1" 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 63ef70ea3..e8442b7f4 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.drm.DummyExoMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.LocalMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -111,6 +112,7 @@ final class BetterPlayer { private WorkManager workManager; private HashMap> workerObserverMap; private CustomDefaultLoadControl customDefaultLoadControl; + private long lastSendBufferedPosition = 0L; BetterPlayer( @@ -147,7 +149,7 @@ void setDataSource( Context context, String key, String dataSource, String formatHint, Result result, Map headers, boolean useCache, long maxCacheSize, long maxCacheFileSize, long overriddenDuration, String licenseUrl, Map drmHeaders, - String cacheKey) { + String cacheKey, String clearKey) { this.key = key; isInitialized = false; @@ -189,6 +191,16 @@ void setDataSource( .build(httpMediaDrmCallback); } } + } else if (clearKey != null && !clearKey.isEmpty()) { + if (Util.SDK_INT < 18) { + Log.e(TAG, "Protected content not supported on API levels below 18"); + drmSessionManager = null; + } else { + drmSessionManager = new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(C.CLEARKEY_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER). + build(new LocalMediaDrmCallback(clearKey.getBytes())); + } + } else { drmSessionManager = null; } @@ -556,8 +568,9 @@ public void onCancel(Object o) { exoPlayer.addListener(new Player.Listener() { @Override public void onPlaybackStateChanged(int playbackState) { + if (playbackState == Player.STATE_BUFFERING) { - sendBufferingUpdate(); + sendBufferingUpdate(true); Map event = new HashMap<>(); event.put("event", "bufferingStart"); eventSink.success(event); @@ -590,13 +603,17 @@ public void onPlayerError(final ExoPlaybackException error) { result.success(reply); } - void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); + void sendBufferingUpdate(boolean isFromBufferingStart) { + long bufferedPosition = exoPlayer.getBufferedPosition(); + if (isFromBufferingStart || bufferedPosition != lastSendBufferedPosition) { + Map event = new HashMap<>(); + event.put("event", "bufferingUpdate"); + List range = Arrays.asList(0, bufferedPosition); + // iOS supports a list of buffered ranges, so here is a list with a single range. + event.put("values", Collections.singletonList(range)); + eventSink.success(event); + lastSendBufferedPosition = bufferedPosition; + } } private void setAudioAttributes(SimpleExoPlayer exoPlayer, Boolean mixWithOthers) { diff --git a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java index 50034f2c5..885700a78 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java @@ -65,6 +65,7 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC private static final String INDEX_PARAMETER = "index"; private static final String LICENSE_URL_PARAMETER = "licenseUrl"; private static final String DRM_HEADERS_PARAMETER = "drmHeaders"; + private static final String DRM_CLEARKEY_PARAMETER = "clearKey"; private static final String MIX_WITH_OTHERS_PARAMETER = "mixWithOthers"; public static final String URL_PARAMETER = "url"; public static final String PRE_CACHE_SIZE_PARAMETER = "preCacheSize"; @@ -238,7 +239,7 @@ private void onMethodCall(MethodCall call, Result result, long textureId, Better break; case POSITION_METHOD: result.success(player.getPosition()); - player.sendBufferingUpdate(); + player.sendBufferingUpdate(false); break; case ABSOLUTE_POSITION_METHOD: result.success(player.getAbsolutePosition()); @@ -305,7 +306,7 @@ private void setDataSource(MethodCall call, Result result, BetterPlayer player) assetLookupKey = flutterState.keyForAsset.get(asset); } - player.setDataSource( + player.setDataSource( flutterState.applicationContext, key, "asset:///" + assetLookupKey, @@ -317,7 +318,7 @@ private void setDataSource(MethodCall call, Result result, BetterPlayer player) 0L, overriddenDuration.longValue(), null, - null, null + null, null, null ); } else { boolean useCache = getParameter(dataSource, USE_CACHE_PARAMETER, false); @@ -329,6 +330,7 @@ private void setDataSource(MethodCall call, Result result, BetterPlayer player) String cacheKey = getParameter(dataSource, CACHE_KEY_PARAMETER, null); String formatHint = getParameter(dataSource, FORMAT_HINT_PARAMETER, null); String licenseUrl = getParameter(dataSource, LICENSE_URL_PARAMETER, null); + String clearKey = getParameter(dataSource, DRM_CLEARKEY_PARAMETER, null); Map drmHeaders = getParameter(dataSource, DRM_HEADERS_PARAMETER, new HashMap<>()); player.setDataSource( flutterState.applicationContext, @@ -343,7 +345,8 @@ private void setDataSource(MethodCall call, Result result, BetterPlayer player) overriddenDuration.longValue(), licenseUrl, drmHeaders, - cacheKey + cacheKey, + clearKey ); } } diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 26c017b26..bcc1f8621 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -12,4 +12,4 @@ - Supports both Android and iOS [GitHub](https://github.com/jhomlala/betterplayer) -[Get Started](#gettingstarted) \ No newline at end of file +[Get Started](#home) \ No newline at end of file diff --git a/docs/controlsconfiguration.md b/docs/controlsconfiguration.md index bff62364d..33651b5f7 100644 --- a/docs/controlsconfiguration.md +++ b/docs/controlsconfiguration.md @@ -157,4 +157,10 @@ final Widget loadingWidget; ///Color of the background, when no frame is displayed. final Color backgroundColor; + +///Quality of Gaussian Blur for x (iOS only). +final double sigmaX; + +///Quality of Gaussian Blur for y (iOS only). +final double sigmaY; ``` \ No newline at end of file diff --git a/docs/drmconfiguration.md b/docs/drmconfiguration.md index b9946134b..1496aa991 100644 --- a/docs/drmconfiguration.md +++ b/docs/drmconfiguration.md @@ -4,7 +4,7 @@ Supported DRMs: * Token based (authorization header): Android/iOS * Widevine (licensue url + headers): Android -* Fairplay EZDRM (certificate url): iOS +* Fairplay EZDRM (certificate url, license url): iOS Additional DRM types may be added in the future. @@ -42,6 +42,48 @@ BetterPlayerDataSource _fairplayDataSource = BetterPlayerDataSource( drmConfiguration: BetterPlayerDrmConfiguration( drmType: BetterPlayerDrmType.fairplay, certificateUrl: Constants.fairplayCertificateUrl, + licenseUrl: Constants.fairplayLicenseUrl, ), ); -``` \ No newline at end of file +``` + +ClearKey (only supported in Android): + +A ClearKey MP4 file can be generated with MP4Box as follow: + +- Create drm_file.xml with the following contents. +```xml + + + + + + + + + + + + + + + + +``` +- Create the mp4 container using [MP4Box](https://gpac.wp.imt.fr/) + - MP4Box -crypt drm_file.xml testvideo.mp4 -out testvideo_encrypt_tmp.mp4 + - MP4Box -frag 240000 testvideo_encrypt_tmp.mp4 -out testvideo_encrypt.mp4 (need to create multi segment mp4 file as ExoPlayer does not read the pssh block on a single segment mp4 file) +```dart + + var _clearKeyDataSourceFile = BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + await Utils.getFileUrl(Constants.fileTestVideoEncryptUrl), + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.clearKey, + clearKey: BetterPlayerClearKeyUtils.generate({ + "f3c5e0361e6654b28f8049c778b23946": + "a4631a153a443df9eed0593043db7519", + "abba271e8bcf552bbd2e86a434a9a5d9": + "69eaa802a6763af979e8d1940fb88392" + })), + ); diff --git a/docs/install.md b/docs/install.md index 1beea7585..22a5202ee 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,7 +4,7 @@ ```yaml dependencies: - better_player: ^0.0.72 + better_player: ^0.0.73 ``` 2. Install it diff --git a/docs/listplayerusage.md b/docs/listplayerusage.md index 940a6ddcd..a7d9eceec 100644 --- a/docs/listplayerusage.md +++ b/docs/listplayerusage.md @@ -21,4 +21,6 @@ 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 +`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 + +To resolve random OOM issues, try to lower values in `bufferingConfiguration` in `BetterPlayerDataSource`. \ 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/subtitlesconfiguration.md b/docs/subtitlesconfiguration.md index f117f3627..4684f4f6e 100644 --- a/docs/subtitlesconfiguration.md +++ b/docs/subtitlesconfiguration.md @@ -140,4 +140,8 @@ final Color backgroundColor; ///Subtitles selected by default, without user interaction final bool selectedByDefault; -``` \ No newline at end of file +``` + +## Current subtitle + +To get currently displayed subtitle, use `renderedSubtitle` in `BetterPlayerController`. \ No newline at end of file diff --git a/example/assets/example_subtitles.srt b/example/assets/example_subtitles.srt index cd05edc94..7531513ab 100644 --- a/example/assets/example_subtitles.srt +++ b/example/assets/example_subtitles.srt @@ -6,108 +6,6 @@ Welcome to Better Player 00:00:02.000 --> 00:00:06.000 Hope you enjoy features in your new video player. -00:00:06.000 --> 01:58:00.000 -Have fun! - 3 -00:01:58.000 --> 00:02:01.450 -Es ist töricht, so ganz allein und -unvorbereitet zu reisen! - -4 -00:02:01.750 --> 00:02:04.800 -Du kannst von Glück sagen, dass dein -Blut noch in deinen Adern fließt. - -5 -00:02:05.250 --> 00:02:06.300 -Danke. - -6 -00:02:07.500 --> 00:02:09.000 -Also... - -7 -00:02:09.400 --> 00:02:13.800 -...was führt dich in die Lande der -Torwaechter? - -8 -00:02:15.000 --> 00:02:17.500 -Ich suche jemanden. - -9 -00:02:18.000 --> 00:02:22.200 -Ein teurer Freund? -Eine verwandte Seele? - -10 -00:02:23.400 --> 00:02:25.000 -Ein Drache. - -11 -00:02:28.850 --> 00:02:31.750 -Ein gefährliches Unterfangen für eine -einsame Jägerin. - -12 -00:02:32.950 --> 00:02:35.870 -Ich bin einsam, solange ich mich -erinnern kann. - -13 -00:03:27.250 --> 00:03:30.500 -Wir sind fast fertig. Ruhig... - -14 -00:03:30.750 --> 00:03:33.500 -He, halt still. - -15 -00:03:48.250 --> 00:03:52.250 -Gute Nacht, Scales. - -16 -00:04:10.350 --> 00:04:13.850 -Los, hol ihn dir, Scales! Komm schon! - -17 -00:04:25.250 --> 00:04:28.250 -Scales? - -18 -00:05:04.000 --> 00:05:07.500 -Ja! Vorwärts! - -19 -00:05:38.750 --> 00:05:42.000 -Scales! - -20 -00:07:25.850 --> 00:07:27.500 -Ich habe versagt. - -21 -00:07:32.800 --> 00:07:36.500 -Du hast es nur nicht vermocht, genauer -hinzusehen. - -22 -00:07:37.800 --> 00:07:40.500 -Dies ist das Reich der Drachen, Sintel. - -23 -00:07:40.850 --> 00:07:44.000 -Du bist näher, als du ahnst. - -24 -00:09:17.600 --> 00:09:19.500 -Scales! - -25 -00:10:21.600 --> 00:10:24.000 -Scales? - -26 -00:10:26.200 --> 00:10:29.800 -Scales... +00:00:06.000 --> 00:00:10.000 +Have fun! diff --git a/example/assets/testvideo_encrypt.mp4 b/example/assets/testvideo_encrypt.mp4 new file mode 100644 index 000000000..e2e3fc518 Binary files /dev/null and b/example/assets/testvideo_encrypt.mp4 differ diff --git a/example/lib/constants.dart b/example/lib/constants.dart index d0e352bdb..ec49d02af 100644 --- a/example/lib/constants.dart +++ b/example/lib/constants.dart @@ -4,6 +4,9 @@ class Constants { static const String forBiggerBlazesUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"; static const String fileTestVideoUrl = "testvideo.mp4"; + static const String fileTestVideoEncryptUrl = "testvideo_encrypt.mp4"; + static const String networkTestVideoEncryptUrl = + "https://github.com/tinusneethling/betterplayer/raw/ClearKeySupport/example/assets/testvideo_encrypt.mp4"; static const String fileExampleSubtitlesUrl = "example_subtitles.srt"; static const String hlsTestStreamUrl = "https://mtoczko.github.io/hls-test-streams/test-group/playlist.m3u8"; @@ -44,6 +47,7 @@ class Constants { "https://fps.ezdrm.com/demo/hls/BigBuckBunny_320x180.m3u8"; static String fairplayCertificateUrl = "https://github.com/koldo92/betterplayer/raw/fairplay_ezdrm/example/assets/eleisure.cer"; + static String fairplayLicenseUrl = "https://fps.ezdrm.com/api/licenses/"; static String catImageUrl = "https://img.webmd.com/dtmcms/live/webmd/consumer_assets/site_images/article_thumbnails/other/cat_relaxing_on_patio_other/1800x1200_cat_relaxing_on_patio_other.jpg"; static String dashStreamUrl = diff --git a/example/lib/pages/clearkey_page.dart b/example/lib/pages/clearkey_page.dart new file mode 100644 index 000000000..8e82721a2 --- /dev/null +++ b/example/lib/pages/clearkey_page.dart @@ -0,0 +1,167 @@ +import 'dart:io'; + +import 'package:better_player/better_player.dart'; +import 'package:flutter/material.dart'; + +import 'package:better_player_example/constants.dart'; +import 'package:better_player_example/utils.dart'; + +class ClearKeyPage extends StatefulWidget { + @override + State createState() => _ClearKeyState(); +} + +class _ClearKeyState extends State { + late BetterPlayerController _clearKeyControllerFile; + late BetterPlayerController _clearKeyControllerBroken; + late BetterPlayerController _clearKeyControllerNetwork; + late BetterPlayerController _clearKeyControllerMemory; + + @override + void initState() { + BetterPlayerConfiguration betterPlayerConfiguration = + BetterPlayerConfiguration( + aspectRatio: 16 / 9, + fit: BoxFit.contain, + ); + _clearKeyControllerFile = BetterPlayerController(betterPlayerConfiguration); + _clearKeyControllerBroken = + BetterPlayerController(betterPlayerConfiguration); + _clearKeyControllerNetwork = + BetterPlayerController(betterPlayerConfiguration); + _clearKeyControllerMemory = + BetterPlayerController(betterPlayerConfiguration); + + _setupDataSources(); + + super.initState(); + } + + void _setupDataSources() async { + var _clearKeyDataSourceFile = BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + await Utils.getFileUrl(Constants.fileTestVideoEncryptUrl), + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.clearKey, + clearKey: BetterPlayerClearKeyUtils.generateKey({ + "f3c5e0361e6654b28f8049c778b23946": + "a4631a153a443df9eed0593043db7519", + "abba271e8bcf552bbd2e86a434a9a5d9": + "69eaa802a6763af979e8d1940fb88392" + })), + ); + + _clearKeyControllerFile.setupDataSource(_clearKeyDataSourceFile); + + BetterPlayerDataSource _clearKeyDataSourceBroken = BetterPlayerDataSource( + BetterPlayerDataSourceType.file, + await Utils.getFileUrl(Constants.fileTestVideoEncryptUrl), + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.clearKey, + clearKey: BetterPlayerClearKeyUtils.generateKey({ + "f3c5e0361e6654b28f8049c778b23946": + "a4631a153a443df9eed0593043d11111", + "abba271e8bcf552bbd2e86a434a9a5d9": + "69eaa802a6763af979e8d1940fb11111" + })), + ); + + _clearKeyControllerBroken.setupDataSource(_clearKeyDataSourceBroken); + + var _clearKeyDataSourceNetwork = BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + Constants.networkTestVideoEncryptUrl, + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.clearKey, + clearKey: BetterPlayerClearKeyUtils.generateKey({ + "f3c5e0361e6654b28f8049c778b23946": + "a4631a153a443df9eed0593043db7519", + "abba271e8bcf552bbd2e86a434a9a5d9": + "69eaa802a6763af979e8d1940fb88392" + })), + ); + + _clearKeyControllerNetwork.setupDataSource(_clearKeyDataSourceNetwork); + + var _clearKeyDataSourceMemory = BetterPlayerDataSource( + BetterPlayerDataSourceType.memory, + "", + bytes: File(await Utils.getFileUrl(Constants.fileTestVideoEncryptUrl)) + .readAsBytesSync(), + drmConfiguration: BetterPlayerDrmConfiguration( + drmType: BetterPlayerDrmType.clearKey, + clearKey: BetterPlayerClearKeyUtils.generateKey({ + "f3c5e0361e6654b28f8049c778b23946": + "a4631a153a443df9eed0593043db7519", + "abba271e8bcf552bbd2e86a434a9a5d9": + "69eaa802a6763af979e8d1940fb88392" + })), + ); + + _clearKeyControllerMemory.setupDataSource(_clearKeyDataSourceMemory); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("ClearKey DRM"), + ), + body: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "ClearKey Protection with valid key.", + style: TextStyle(fontSize: 16), + ), + ), + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _clearKeyControllerFile), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "ClearKey Protection with invalid key.", + style: TextStyle(fontSize: 16), + ), + ), + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _clearKeyControllerBroken), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "ClearKey Protection Network with valid key.", + style: TextStyle(fontSize: 16), + ), + ), + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _clearKeyControllerNetwork), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "ClearKey Protection Asset with valid key.", + style: TextStyle(fontSize: 16), + ), + ), + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _clearKeyControllerMemory), + ), + const SizedBox(height: 100), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/drm_page.dart b/example/lib/pages/drm_page.dart index 0df626841..b8fbe1fec 100644 --- a/example/lib/pages/drm_page.dart +++ b/example/lib/pages/drm_page.dart @@ -48,6 +48,7 @@ class _DrmPageState extends State { drmConfiguration: BetterPlayerDrmConfiguration( drmType: BetterPlayerDrmType.fairplay, certificateUrl: Constants.fairplayCertificateUrl, + licenseUrl: Constants.fairplayLicenseUrl, ), ); _fairplayController.setupDataSource(_fairplayDataSource); diff --git a/example/lib/pages/hls_subtitles_page.dart b/example/lib/pages/hls_subtitles_page.dart index f86b20954..464dc74a6 100644 --- a/example/lib/pages/hls_subtitles_page.dart +++ b/example/lib/pages/hls_subtitles_page.dart @@ -12,11 +12,42 @@ class _HlsSubtitlesPageState extends State { @override void initState() { + BetterPlayerControlsConfiguration controlsConfiguration = + BetterPlayerControlsConfiguration( + controlBarColor: Colors.black26, + iconsColor: Colors.white, + playIcon: Icons.play_arrow_outlined, + progressBarPlayedColor: Colors.indigo, + progressBarHandleColor: Colors.indigo, + skipBackIcon: Icons.replay_10_outlined, + skipForwardIcon: Icons.forward_10_outlined, + backwardSkipTimeInMilliseconds: 10000, + forwardSkipTimeInMilliseconds: 10000, + enableSkips: true, + enableFullscreen: true, + enablePip: true, + enablePlayPause: true, + enableMute: true, + enableAudioTracks: true, + enableProgressText: true, + enableSubtitles: true, + showControlsOnInitialize: true, + enablePlaybackSpeed: true, + controlBarHeight: 40, + loadingColor: Colors.red, + overflowModalColor: Colors.black54, + overflowModalTextColor: Colors.white, + overflowMenuIconsColor: Colors.white, + ); + BetterPlayerConfiguration betterPlayerConfiguration = BetterPlayerConfiguration( - aspectRatio: 16 / 9, - fit: BoxFit.contain, - ); + controlsConfiguration: controlsConfiguration, + aspectRatio: 16 / 9, + fit: BoxFit.contain, + subtitlesConfiguration: BetterPlayerSubtitlesConfiguration( + fontSize: 16.0, + )); BetterPlayerDataSource dataSource = BetterPlayerDataSource( BetterPlayerDataSourceType.network, Constants.hlsPlaylistUrl, useAsmsSubtitles: true); @@ -31,22 +62,24 @@ class _HlsSubtitlesPageState extends State { appBar: AppBar( title: Text("HLS subtitles"), ), - body: Column( - children: [ - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - "Player with HLS stream which loads subtitles from HLS." - " You can choose subtitles by using overflow menu (3 dots in right corner).", - style: TextStyle(fontSize: 16), + body: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + "Player with HLS stream which loads subtitles from HLS." + " You can choose subtitles by using overflow menu (3 dots in right corner).", + style: TextStyle(fontSize: 16), + ), ), - ), - AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer(controller: _betterPlayerController), - ), - ], + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _betterPlayerController), + ), + ], + ), ), ); } diff --git a/example/lib/pages/normal_player_page.dart b/example/lib/pages/normal_player_page.dart index 99df9acfa..8ea1f2701 100644 --- a/example/lib/pages/normal_player_page.dart +++ b/example/lib/pages/normal_player_page.dart @@ -18,10 +18,11 @@ class _NormalPlayerPageState extends State { aspectRatio: 16 / 9, fit: BoxFit.contain, autoPlay: true, + looping: true, ); _betterPlayerDataSource = BetterPlayerDataSource( BetterPlayerDataSourceType.network, - Constants.elephantDreamVideoUrl, + Constants.forBiggerBlazesUrl, ); _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); _betterPlayerController.setupDataSource(_betterPlayerDataSource); @@ -37,14 +38,9 @@ class _NormalPlayerPageState extends State { body: Column( children: [ const SizedBox(height: 8), - BetterPlayerMultipleGestureDetector( - child: AspectRatio( - aspectRatio: 16 / 9, - child: BetterPlayer(controller: _betterPlayerController), - ), - onTap: () { - print("Tap!"); - }, + AspectRatio( + aspectRatio: 16 / 9, + child: BetterPlayer(controller: _betterPlayerController), ), ], ), diff --git a/example/lib/pages/playlist_page.dart b/example/lib/pages/playlist_page.dart index be688eb9f..df79c2194 100644 --- a/example/lib/pages/playlist_page.dart +++ b/example/lib/pages/playlist_page.dart @@ -30,7 +30,7 @@ class _PlaylistPageState extends State { ); _betterPlayerPlaylistConfiguration = BetterPlayerPlaylistConfiguration( loopVideos: true, - nextVideoDelay: Duration(seconds: 1), + nextVideoDelay: Duration(seconds: 3), ); } diff --git a/example/lib/pages/subtitles_page.dart b/example/lib/pages/subtitles_page.dart index 54a03e9c2..2459e8b55 100644 --- a/example/lib/pages/subtitles_page.dart +++ b/example/lib/pages/subtitles_page.dart @@ -26,6 +26,12 @@ class _SubtitlesPageState extends State { ); _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); + _betterPlayerController.addEventsListener((event) { + if (event.betterPlayerEventType == BetterPlayerEventType.progress) { + print("Current subtitle line: " + + _betterPlayerController.renderedSubtitle.toString()); + } + }); _setupDataSource(); super.initState(); } diff --git a/example/lib/pages/video_list/video_list_widget.dart b/example/lib/pages/video_list/video_list_widget.dart index 0957d29f2..fec693e07 100644 --- a/example/lib/pages/video_list/video_list_widget.dart +++ b/example/lib/pages/video_list/video_list_widget.dart @@ -52,6 +52,11 @@ class _VideoListWidgetState extends State { showNotification: true, title: videoListData!.videoTitle, author: "Test"), + bufferingConfiguration: BetterPlayerBufferingConfiguration( + minBufferMs: 2000, + maxBufferMs: 10000, + bufferForPlaybackMs: 1000, + bufferForPlaybackAfterRebufferMs: 2000), ), configuration: BetterPlayerConfiguration( autoPlay: false, diff --git a/example/lib/pages/welcome_page.dart b/example/lib/pages/welcome_page.dart index d3e51db71..bad138ec2 100644 --- a/example/lib/pages/welcome_page.dart +++ b/example/lib/pages/welcome_page.dart @@ -4,6 +4,7 @@ import 'package:better_player_example/constants.dart'; import 'package:better_player_example/pages/auto_fullscreen_orientation_page.dart'; import 'package:better_player_example/pages/basic_player_page.dart'; import 'package:better_player_example/pages/cache_page.dart'; +import 'package:better_player_example/pages/clearkey_page.dart'; import 'package:better_player_example/pages/controller_controls_page.dart'; import 'package:better_player_example/pages/controls_always_visible_page.dart'; import 'package:better_player_example/pages/controls_configuration_page.dart'; @@ -42,6 +43,7 @@ class _WelcomePageState extends State { void initState() { _saveAssetSubtitleToFile(); _saveAssetVideoToFile(); + _saveAssetEncryptVideoToFile(); _saveLogoToFile(); super.initState(); } @@ -127,9 +129,6 @@ class _WelcomePageState extends State { _buildExampleElementWidget("Overridden aspect ratio", () { _navigateToPage(OverriddenAspectRatioPage()); }), - _buildExampleElementWidget("Overridden aspect ratio", () { - _navigateToPage(OverriddenAspectRatioPage()); - }), _buildExampleElementWidget("Notifications player", () { _navigateToPage(NotificationPlayerPage()); }), @@ -157,6 +156,9 @@ class _WelcomePageState extends State { _buildExampleElementWidget("DRM", () { _navigateToPage(DrmPage()); }), + _buildExampleElementWidget("ClearKey DRM", () { + _navigateToPage(ClearKeyPage()); + }), _buildExampleElementWidget("DASH", () { _navigateToPage(DashPage()); }), @@ -208,6 +210,14 @@ class _WelcomePageState extends State { file.writeAsBytesSync(content.buffer.asUint8List()); } + Future _saveAssetEncryptVideoToFile() async { + var content = + await rootBundle.load("assets/${Constants.fileTestVideoEncryptUrl}"); + final directory = await getApplicationDocumentsDirectory(); + var file = File("${directory.path}/${Constants.fileTestVideoEncryptUrl}"); + file.writeAsBytesSync(content.buffer.asUint8List()); + } + ///Save logo to file, so we can use it later Future _saveLogoToFile() async { var content = await rootBundle.load("assets/${Constants.logo}"); diff --git a/ios/Classes/BetterPlayer.h b/ios/Classes/BetterPlayer.h index 95845bf24..aceb5833d 100644 --- a/ios/Classes/BetterPlayer.h +++ b/ios/Classes/BetterPlayer.h @@ -44,8 +44,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithFrame:(CGRect)frame; - (void)setMixWithOthers:(bool)mixWithOthers; - (void)seekTo:(int)location; -- (void)setDataSourceAsset:(NSString*)asset withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl overriddenDuration:(int) overriddenDuration; -- (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withHeaders:(NSDictionary*)headers withCache:(BOOL)useCache overriddenDuration:(int) overriddenDuration; +- (void)setDataSourceAsset:(NSString*)asset withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withLicenseUrl:(NSString*)licenseUrl overriddenDuration:(int) overriddenDuration; +- (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withLicenseUrl:(NSString*)licenseUrl withHeaders:(NSDictionary*)headers withCache:(BOOL)useCache overriddenDuration:(int) overriddenDuration; - (void)setVolume:(double)volume; - (void)setSpeed:(double)speed result:(FlutterResult)result; - (void) setAudioTrack:(NSString*) name index:(int) index; diff --git a/ios/Classes/BetterPlayer.m b/ios/Classes/BetterPlayer.m index 5625f7255..326e0f2d5 100644 --- a/ios/Classes/BetterPlayer.m +++ b/ios/Classes/BetterPlayer.m @@ -187,12 +187,12 @@ - (CGAffineTransform)fixTransform:(AVAssetTrack*)videoTrack { return transform; } -- (void)setDataSourceAsset:(NSString*)asset withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl overriddenDuration:(int) overriddenDuration{ +- (void)setDataSourceAsset:(NSString*)asset withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withLicenseUrl:(NSString*)licenseUrl overriddenDuration:(int) overriddenDuration{ NSString* path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; - return [self setDataSourceURL:[NSURL fileURLWithPath:path] withKey:key withCertificateUrl:certificateUrl withHeaders: @{} withCache: false overriddenDuration:overriddenDuration]; + return [self setDataSourceURL:[NSURL fileURLWithPath:path] withKey:key withCertificateUrl:certificateUrl withLicenseUrl:licenseUrl withHeaders: @{} withCache: false overriddenDuration:overriddenDuration]; } -- (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withHeaders:(NSDictionary*)headers withCache:(BOOL)useCache overriddenDuration:(int) overriddenDuration{ +- (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withCertificateUrl:(NSString*)certificateUrl withLicenseUrl:(NSString*)licenseUrl withHeaders:(NSDictionary*)headers withCache:(BOOL)useCache overriddenDuration:(int) overriddenDuration{ _overriddenDuration = 0; if (headers == [NSNull null]){ headers = @{}; @@ -207,7 +207,10 @@ - (void)setDataSourceURL:(NSURL*)url withKey:(NSString*)key withCertificateUrl:( options:@{@"AVURLAssetHTTPHeaderFieldsKey" : headers}]; if (certificateUrl && certificateUrl != [NSNull null] && [certificateUrl length] > 0) { - _loaderDelegate = [[BetterPlayerEzDrmAssetsLoaderDelegate alloc] initWithCertificateUrl:[[NSURL alloc] initWithString: certificateUrl]]; + NSURL * certificateNSURL = [[NSURL alloc] initWithString: certificateUrl]; + NSURL * licenseNSURL = [[NSURL alloc] initWithString: licenseUrl]; + + _loaderDelegate = [[BetterPlayerEzDrmAssetsLoaderDelegate alloc] init:certificateNSURL withLicenseURL:licenseNSURL]; dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, -1); dispatch_queue_t streamQueue = dispatch_queue_create("streamQueue", qos); [asset.resourceLoader setDelegate:_loaderDelegate queue:streamQueue]; diff --git a/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.h b/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.h index 649051d85..465c6f25d 100644 --- a/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.h +++ b/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.h @@ -9,6 +9,7 @@ @interface BetterPlayerEzDrmAssetsLoaderDelegate : NSObject @property(readonly, nonatomic) NSURL* certificateURL; -- (instancetype)initWithCertificateUrl:(NSURL*)certificateUrl; +@property(readonly, nonatomic) NSURL* licenseURL; +- (instancetype)init:(NSURL *)certificateURL withLicenseURL:(NSURL *)licenseURL; @end diff --git a/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.m b/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.m index 07b61eb63..6f5551e6b 100644 --- a/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.m +++ b/ios/Classes/BetterPlayerEzDrmAssetsLoaderDelegate.m @@ -8,11 +8,12 @@ @implementation BetterPlayerEzDrmAssetsLoaderDelegate NSString *_assetId; -NSString * KEY_SERVER_URL = @"https://fps.ezdrm.com/api/licenses/"; +NSString * DEFAULT_LICENSE_SERVER_URL = @"https://fps.ezdrm.com/api/licenses/"; -- (instancetype)initWithCertificateUrl:(NSURL *)certificateUrl { +- (instancetype)init:(NSURL *)certificateURL withLicenseURL:(NSURL *)licenseURL{ self = [super init]; - _certificateURL = certificateUrl; + _certificateURL = certificateURL; + _licenseURL = licenseURL; return self; } @@ -20,14 +21,20 @@ - (instancetype)initWithCertificateUrl:(NSURL *)certificateUrl { ** ** getContentKeyAndLeaseExpiryFromKeyServerModuleWithRequest ** - ** takes the bundled SPC and sends it the the - ** key server defined at KEY_SERVER_URL in the View Controller - ** it returns a CKC which then is returned. + ** Takes the bundled SPC and sends it to the license server defined at licenseUrl or KEY_SERVER_URL (if licenseUrl is null). + ** It returns CKC. ** ---------------------------------------*/ - (NSData *)getContentKeyAndLeaseExpiryFromKeyServerModuleWithRequest:(NSData*)requestBytes and:(NSString *)assetId and:(NSString *)customParams and:(NSError *)errorOut { NSData * decodedData; NSURLResponse * response; - NSURL * ksmURL = [[NSURL alloc] initWithString: [NSString stringWithFormat:@"%@%@%@",KEY_SERVER_URL,assetId,customParams]]; + + NSURL * finalLicenseURL; + if (_licenseURL != [NSNull null]){ + finalLicenseURL = _licenseURL; + } else { + finalLicenseURL = [[NSURL alloc] initWithString: DEFAULT_LICENSE_SERVER_URL]; + } + NSURL * ksmURL = [[NSURL alloc] initWithString: [NSString stringWithFormat:@"%@%@%@",finalLicenseURL,assetId,customParams]]; NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:ksmURL]; [request setHTTPMethod:@"POST"]; diff --git a/ios/Classes/BetterPlayerPlugin.m b/ios/Classes/BetterPlayerPlugin.m index 389a6b0e1..1c3fea89b 100644 --- a/ios/Classes/BetterPlayerPlugin.m +++ b/ios/Classes/BetterPlayerPlugin.m @@ -15,6 +15,8 @@ @implementation BetterPlayerPlugin NSMutableDictionary* _timeObserverIdDict; NSMutableDictionary* _artworkImageDict; int texturesCount = -1; +BetterPlayer* _notificationPlayer; +bool _remoteCommandsInitialized = false; #pragma mark - FlutterPlugin protocol @@ -82,6 +84,7 @@ - (void)onPlayerSetup:(BetterPlayer*)player } - (void) setupRemoteNotification :(BetterPlayer*) player{ + _notificationPlayer = player; [self stopOtherUpdateListener:player]; NSDictionary* dataSource = [_dataSourceDict objectForKey:[self getTextureId:player]]; BOOL showNotification = false; @@ -116,6 +119,9 @@ - (void) setRemoteCommandsNotificationNotActive{ - (void) setupRemoteCommands:(BetterPlayer*)player { + if (_remoteCommandsInitialized){ + return; + } MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; [commandCenter.togglePlayPauseCommand setEnabled:YES]; [commandCenter.playCommand setEnabled:YES]; @@ -127,22 +133,28 @@ - (void) setupRemoteCommands:(BetterPlayer*)player { } [commandCenter.togglePlayPauseCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - if (player.isPlaying){ - player.eventSink(@{@"event" : @"play"}); - } else { - player.eventSink(@{@"event" : @"pause"}); - + if (_notificationPlayer != [NSNull null]){ + if (_notificationPlayer.isPlaying){ + _notificationPlayer.eventSink(@{@"event" : @"play"}); + } else { + _notificationPlayer.eventSink(@{@"event" : @"pause"}); + + } } return MPRemoteCommandHandlerStatusSuccess; }]; [commandCenter.playCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - player.eventSink(@{@"event" : @"play"}); + if (_notificationPlayer != [NSNull null]){ + _notificationPlayer.eventSink(@{@"event" : @"play"}); + } return MPRemoteCommandHandlerStatusSuccess; }]; [commandCenter.pauseCommand addTargetWithHandler: ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - player.eventSink(@{@"event" : @"pause"}); + if (_notificationPlayer != [NSNull null]){ + _notificationPlayer.eventSink(@{@"event" : @"pause"}); + } return MPRemoteCommandHandlerStatusSuccess; }]; @@ -150,16 +162,18 @@ - (void) setupRemoteCommands:(BetterPlayer*)player { if (@available(iOS 9.1, *)) { [commandCenter.changePlaybackPositionCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { - - MPChangePlaybackPositionCommandEvent * playbackEvent = (MPChangePlaybackRateCommandEvent * ) event; - CMTime time = CMTimeMake(playbackEvent.positionTime, 1); - int64_t millis = [BetterPlayerTimeUtils FLTCMTimeToMillis:(time)]; - [player seekTo: millis]; - player.eventSink(@{@"event" : @"seek", @"position": @(millis)}); + if (_notificationPlayer != [NSNull null]){ + MPChangePlaybackPositionCommandEvent * playbackEvent = (MPChangePlaybackRateCommandEvent * ) event; + CMTime time = CMTimeMake(playbackEvent.positionTime, 1); + int64_t millis = [BetterPlayerTimeUtils FLTCMTimeToMillis:(time)]; + [_notificationPlayer seekTo: millis]; + _notificationPlayer.eventSink(@{@"event" : @"seek", @"position": @(millis)}); + } return MPRemoteCommandHandlerStatusSuccess; }]; } + _remoteCommandsInitialized = true; } - (void) setupRemoteCommandNotification:(BetterPlayer*)player, NSString* title, NSString* author , NSString* imageUrl{ @@ -232,6 +246,10 @@ - (void) setupUpdateListener:(BetterPlayer*)player,NSString* title, NSString* au - (void) disposeNotificationData: (BetterPlayer*)player{ + if (player == _notificationPlayer){ + _notificationPlayer = NULL; + _remoteCommandsInitialized = false; + } NSString* key = [self getTextureId:player]; id _timeObserverId = _timeObserverIdDict[key]; [_timeObserverIdDict removeObjectForKey: key]; @@ -287,7 +305,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSString* uriArg = dataSource[@"uri"]; NSString* key = dataSource[@"key"]; NSString* certificateUrl = dataSource[@"certificateUrl"]; + NSString* licenseUrl = dataSource[@"licenseUrl"]; NSDictionary* headers = dataSource[@"headers"]; + int overriddenDuration = 0; if ([dataSource objectForKey:@"overriddenDuration"] != [NSNull null]){ overriddenDuration = [dataSource[@"overriddenDuration"] intValue]; @@ -310,9 +330,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else { assetPath = [_registrar lookupKeyForAsset:assetArg]; } - [player setDataSourceAsset:assetPath withKey:key withCertificateUrl:certificateUrl overriddenDuration:overriddenDuration]; + [player setDataSourceAsset:assetPath withKey:key withCertificateUrl:certificateUrl withLicenseUrl: licenseUrl overriddenDuration:overriddenDuration]; } else if (uriArg) { - [player setDataSourceURL:[NSURL URLWithString:uriArg] withKey:key withCertificateUrl:certificateUrl withHeaders:headers withCache: useCache overriddenDuration:overriddenDuration]; + [player setDataSourceURL:[NSURL URLWithString:uriArg] withKey:key withCertificateUrl:certificateUrl withLicenseUrl: licenseUrl withHeaders:headers withCache: useCache overriddenDuration:overriddenDuration]; } else { result(FlutterMethodNotImplemented); } diff --git a/lib/better_player.dart b/lib/better_player.dart index 856ef890c..d20e1d00b 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/clearkey/better_player_clearkey_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'; diff --git a/lib/src/clearkey/better_player_clearkey_utils.dart b/lib/src/clearkey/better_player_clearkey_utils.dart new file mode 100644 index 000000000..a36f13e11 --- /dev/null +++ b/lib/src/clearkey/better_player_clearkey_utils.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +import 'dart:typed_data'; + +///ClearKey helper class to generate the key +class BetterPlayerClearKeyUtils { + static final _byteMask = BigInt.from(0xff); + + ///The ClearKey from a Map. The key in map should be the kid with the associated value being the key. Both values should be provide in HEX format. + static String generateKey(Map keys, + {String type = "temporary"}) { + final Map keyMap = {"type": type}; + keyMap["keys"] = >[]; + keys.forEach((key, value) => keyMap["keys"] + .add({"kty": "oct", "kid": _base64(key), "k": _base64(value)})); + + return jsonEncode(keyMap); + } + + static String _base64(String source) { + return base64 + .encode(_encodeBigInt(BigInt.parse(source, radix: 16))) + .replaceAll("=", ""); + } + + static Uint8List _encodeBigInt(BigInt number) { + var passedNumber = number; + final int size = (number.bitLength + 7) >> 3; + + final result = Uint8List(size); + int pos = size - 1; + for (int i = 0; i < size; i++) { + result[pos--] = (passedNumber & _byteMask).toInt(); + passedNumber = passedNumber >> 8; + } + return result; + } +} diff --git a/lib/src/configuration/better_player_controls_configuration.dart b/lib/src/configuration/better_player_controls_configuration.dart index a5543cab4..892b2f3cf 100644 --- a/lib/src/configuration/better_player_controls_configuration.dart +++ b/lib/src/configuration/better_player_controls_configuration.dart @@ -168,58 +168,67 @@ class BetterPlayerControlsConfiguration { ///Color of text in bottom modal sheet used for overflow menu items. final Color overflowModalTextColor; - const BetterPlayerControlsConfiguration( - {this.controlBarColor = Colors.black87, - this.textColor = Colors.white, - this.iconsColor = Colors.white, - this.playIcon = Icons.play_arrow, - this.pauseIcon = Icons.pause, - this.muteIcon = Icons.volume_up, - this.unMuteIcon = Icons.volume_mute, - this.fullscreenEnableIcon = Icons.fullscreen, - this.fullscreenDisableIcon = Icons.fullscreen_exit, - this.skipBackIcon = Icons.fast_rewind, - this.skipForwardIcon = Icons.fast_forward, - this.enableFullscreen = true, - this.enableMute = true, - this.enableProgressText = true, - this.enableProgressBar = true, - this.enableProgressBarDrag = true, - this.enablePlayPause = true, - this.enableSkips = true, - this.enableAudioTracks = true, - this.progressBarPlayedColor = Colors.white, - this.progressBarHandleColor = Colors.white, - this.progressBarBufferedColor = Colors.white70, - this.progressBarBackgroundColor = Colors.white60, - this.controlsHideTime = const Duration(milliseconds: 300), - this.customControlsBuilder, - this.playerTheme, - this.showControls = true, - this.showControlsOnInitialize = true, - this.controlBarHeight = 48.0, - this.liveTextColor = Colors.red, - this.enableOverflowMenu = true, - this.enablePlaybackSpeed = true, - this.enableSubtitles = true, - this.enableQualities = true, - this.enablePip = true, - this.enableRetry = true, - this.overflowMenuCustomItems = const [], - this.overflowMenuIcon = Icons.more_vert, - this.pipMenuIcon = Icons.picture_in_picture, - this.playbackSpeedIcon = Icons.shutter_speed, - this.qualitiesIcon = Icons.hd, - this.subtitlesIcon = Icons.text_fields, - this.audioTracksIcon = Icons.audiotrack, - this.overflowMenuIconsColor = Colors.black, - this.forwardSkipTimeInMilliseconds = 15000, - this.backwardSkipTimeInMilliseconds = 15000, - this.loadingColor = Colors.white, - this.loadingWidget, - this.backgroundColor = Colors.black, - this.overflowModalColor = Colors.white, - this.overflowModalTextColor = Colors.black}); + ///Quality of Gaussian Blur for x (iOS only). + final double sigmaX; + + ///Quality of Gaussian Blur for y (iOS only). + final double sigmaY; + + const BetterPlayerControlsConfiguration({ + this.controlBarColor = Colors.black87, + this.textColor = Colors.white, + this.iconsColor = Colors.white, + this.playIcon = Icons.play_arrow_outlined, + this.pauseIcon = Icons.pause_outlined, + this.muteIcon = Icons.volume_up_outlined, + this.unMuteIcon = Icons.volume_off_outlined, + this.fullscreenEnableIcon = Icons.fullscreen_outlined, + this.fullscreenDisableIcon = Icons.fullscreen_exit_outlined, + this.skipBackIcon = Icons.replay_10_outlined, + this.skipForwardIcon = Icons.forward_10_outlined, + this.enableFullscreen = true, + this.enableMute = true, + this.enableProgressText = true, + this.enableProgressBar = true, + this.enableProgressBarDrag = true, + this.enablePlayPause = true, + this.enableSkips = true, + this.enableAudioTracks = true, + this.progressBarPlayedColor = Colors.white, + this.progressBarHandleColor = Colors.white, + this.progressBarBufferedColor = Colors.white70, + this.progressBarBackgroundColor = Colors.white60, + this.controlsHideTime = const Duration(milliseconds: 300), + this.customControlsBuilder, + this.playerTheme, + this.showControls = true, + this.showControlsOnInitialize = true, + this.controlBarHeight = 48.0, + this.liveTextColor = Colors.red, + this.enableOverflowMenu = true, + this.enablePlaybackSpeed = true, + this.enableSubtitles = true, + this.enableQualities = true, + this.enablePip = true, + this.enableRetry = true, + this.overflowMenuCustomItems = const [], + this.overflowMenuIcon = Icons.more_vert_outlined, + this.pipMenuIcon = Icons.picture_in_picture_outlined, + this.playbackSpeedIcon = Icons.shutter_speed_outlined, + this.qualitiesIcon = Icons.hd_outlined, + this.subtitlesIcon = Icons.closed_caption_outlined, + this.audioTracksIcon = Icons.audiotrack_outlined, + this.overflowMenuIconsColor = Colors.black, + this.forwardSkipTimeInMilliseconds = 10000, + this.backwardSkipTimeInMilliseconds = 10000, + this.loadingColor = Colors.white, + this.loadingWidget, + this.backgroundColor = Colors.black, + this.overflowModalColor = Colors.white, + this.overflowModalTextColor = Colors.black, + this.sigmaX = 10.0, + this.sigmaY = 10.0, + }); factory BetterPlayerControlsConfiguration.white() { return const BetterPlayerControlsConfiguration( @@ -234,9 +243,12 @@ class BetterPlayerControlsConfiguration { factory BetterPlayerControlsConfiguration.cupertino() { return const BetterPlayerControlsConfiguration( - fullscreenEnableIcon: CupertinoIcons.fullscreen, - fullscreenDisableIcon: CupertinoIcons.fullscreen_exit, - playIcon: CupertinoIcons.play_arrow_solid, - pauseIcon: CupertinoIcons.pause_solid); + fullscreenEnableIcon: CupertinoIcons.arrow_up_left_arrow_down_right, + fullscreenDisableIcon: CupertinoIcons.arrow_down_right_arrow_up_left, + playIcon: CupertinoIcons.play_arrow_solid, + pauseIcon: CupertinoIcons.pause_solid, + skipBackIcon: CupertinoIcons.gobackward_15, + skipForwardIcon: CupertinoIcons.goforward_15, + ); } } diff --git a/lib/src/configuration/better_player_drm_configuration.dart b/lib/src/configuration/better_player_drm_configuration.dart index 79fdc9184..1145145fb 100644 --- a/lib/src/configuration/better_player_drm_configuration.dart +++ b/lib/src/configuration/better_player_drm_configuration.dart @@ -8,20 +8,23 @@ class BetterPlayerDrmConfiguration { ///Parameter used only for token encrypted DRMs final String? token; - ///Url of license server, used only for WIDEVINE/PLAYREADY DRM + ///Url of license server final String? licenseUrl; ///Url of fairplay certificate final String? certificateUrl; + ///ClearKey json object, used only for ClearKey protection. Only support for Android. + final String? clearKey; + ///Additional headers send with auth request, used only for WIDEVINE DRM final Map? headers; - BetterPlayerDrmConfiguration({ - this.drmType, - this.token, - this.licenseUrl, - this.certificateUrl, - this.headers, - }); + BetterPlayerDrmConfiguration( + {this.drmType, + this.token, + this.licenseUrl, + this.certificateUrl, + this.headers, + this.clearKey}); } diff --git a/lib/src/configuration/better_player_drm_type.dart b/lib/src/configuration/better_player_drm_type.dart index 6f9823462..827afa2a1 100644 --- a/lib/src/configuration/better_player_drm_type.dart +++ b/lib/src/configuration/better_player_drm_type.dart @@ -2,4 +2,5 @@ ///token -> supported for both iOS/Android ///widevine -> supported only for Android ///fairplay -> suppoted only for iOS -enum BetterPlayerDrmType { token, widevine, fairplay } +///clearKey -> supported only for Android +enum BetterPlayerDrmType { token, widevine, fairplay, clearKey } diff --git a/lib/src/controls/better_player_controls_state.dart b/lib/src/controls/better_player_controls_state.dart index 8fd977a69..3b393d852 100644 --- a/lib/src/controls/better_player_controls_state.dart +++ b/lib/src/controls/better_player_controls_state.dart @@ -1,4 +1,5 @@ // Dart imports: +import 'dart:io'; import 'dart:math'; // Project imports: @@ -11,6 +12,7 @@ import 'package:better_player/src/video_player/video_player.dart'; // Flutter imports: import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; ///Base class for both material and cupertino controls @@ -123,6 +125,7 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), child: Row( children: [ + const SizedBox(width: 8), Icon( icon, color: betterPlayerControlsConfiguration.overflowMenuIconsColor, @@ -164,6 +167,14 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: [ + SizedBox(width: isSelected ? 8 : 16), + Visibility( + visible: isSelected, + child: Icon( + Icons.check_outlined, + color: + betterPlayerControlsConfiguration.overflowModalTextColor, + )), const SizedBox(width: 16), Text( "$value x", @@ -233,6 +244,14 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: [ + SizedBox(width: isSelected ? 8 : 16), + Visibility( + visible: isSelected, + child: Icon( + Icons.check_outlined, + color: + betterPlayerControlsConfiguration.overflowModalTextColor, + )), const SizedBox(width: 16), Text( subtitlesSource.type == BetterPlayerSubtitlesSourceType.none @@ -308,6 +327,14 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: [ + SizedBox(width: isSelected ? 8 : 16), + Visibility( + visible: isSelected, + child: Icon( + Icons.check_outlined, + color: + betterPlayerControlsConfiguration.overflowModalTextColor, + )), const SizedBox(width: 16), Text( trackName, @@ -331,6 +358,14 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: [ + SizedBox(width: isSelected ? 8 : 16), + Visibility( + visible: isSelected, + child: Icon( + Icons.check_outlined, + color: + betterPlayerControlsConfiguration.overflowModalTextColor, + )), const SizedBox(width: 16), Text( name, @@ -382,6 +417,14 @@ abstract class BetterPlayerControlsState padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Row( children: [ + SizedBox(width: isSelected ? 8 : 16), + Visibility( + visible: isSelected, + child: Icon( + Icons.check_outlined, + color: + betterPlayerControlsConfiguration.overflowModalTextColor, + )), const SizedBox(width: 16), Text( audioTrack.label!, @@ -396,24 +439,78 @@ abstract class BetterPlayerControlsState TextStyle _getOverflowMenuElementTextStyle(bool isSelected) { return TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: betterPlayerControlsConfiguration.overflowModalTextColor, + color: isSelected + ? betterPlayerControlsConfiguration.overflowModalTextColor + : betterPlayerControlsConfiguration.overflowModalTextColor + .withOpacity(0.7), ); } void _showModalBottomSheet(List children) { + Platform.isAndroid + ? _showMaterialBottomSheet(children) + : _showCupertinoModalBottomSheet(children); + } + + void _showCupertinoModalBottomSheet(List children) { + showCupertinoModalPopup( + barrierColor: Colors.transparent, + context: context, + builder: (context) { + return SafeArea( + top: false, + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + decoration: BoxDecoration( + color: betterPlayerControlsConfiguration.overflowModalColor, + /*shape: RoundedRectangleBorder(side: Bor,borderRadius: 24,)*/ + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24.0), + topRight: Radius.circular(24.0)), + ), + child: Column( + children: children, + ), + ), + ), + ); + }, + ); + } + + void _showMaterialBottomSheet(List children) { showModalBottomSheet( - backgroundColor: betterPlayerControlsConfiguration.overflowModalColor, + backgroundColor: Colors.transparent, context: context, builder: (context) { return SafeArea( top: false, child: SingleChildScrollView( - child: Column( - children: children, + physics: const BouncingScrollPhysics(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + decoration: BoxDecoration( + color: betterPlayerControlsConfiguration.overflowModalColor, + /*shape: RoundedRectangleBorder(side: Bor,borderRadius: 24,)*/ + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24.0), + topRight: Radius.circular(24.0)), + ), + child: Column( + children: children, + ), ), ), ); }, ); } + + ///Builds directionality widget which wraps child widget and forces left to + ///right directionality. + Widget buildLTRDirectionality(Widget child) { + return Directionality(textDirection: TextDirection.ltr, child: child); + } } diff --git a/lib/src/controls/better_player_cupertino_controls.dart b/lib/src/controls/better_player_cupertino_controls.dart index 78fe5c8de..2d02082a7 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:flutter/cupertino.dart'; import 'package:better_player/src/controls/better_player_multiple_gesture_detector.dart'; import 'package:flutter/material.dart'; @@ -64,6 +65,11 @@ class _BetterPlayerCupertinoControlsState @override Widget build(BuildContext context) { + return buildLTRDirectionality(_buildMainWidget()); + } + + ///Builds main widget of the controls. + Widget _buildMainWidget() { _betterPlayerController = BetterPlayerController.of(context); if (_latestValue?.hasError == true) { @@ -80,8 +86,11 @@ class _BetterPlayerCupertinoControlsState final orientation = MediaQuery.of(context).orientation; final barHeight = orientation == Orientation.portrait ? _controlsConfiguration.controlBarHeight - : _controlsConfiguration.controlBarHeight + 17; - final buttonPadding = orientation == Orientation.portrait ? 16.0 : 24.0; + : _controlsConfiguration.controlBarHeight + 10; + const buttonPadding = 10.0; + final sigmaX = _controlsConfiguration.sigmaX; + final sigmaY = _controlsConfiguration.sigmaY; + _wasLoading = isLoading(_latestValue); return GestureDetector( onTap: () { @@ -110,13 +119,26 @@ class _BetterPlayerCupertinoControlsState absorbing: _hideStuff, child: Column( children: [ - _buildTopBar(backgroundColor, iconColor, barHeight, buttonPadding), + _buildTopBar( + backgroundColor, + iconColor, + barHeight, + buttonPadding, + sigmaX, + sigmaY, + ), if (_wasLoading) Expanded(child: Center(child: _buildLoadingWidget())) else _buildHitArea(), _buildNextVideoWidget(), - _buildBottomBar(backgroundColor, iconColor, barHeight), + _buildBottomBar( + backgroundColor, + iconColor, + barHeight, + sigmaX, + sigmaY, + ), ], ), ), @@ -155,6 +177,8 @@ class _BetterPlayerCupertinoControlsState Color backgroundColor, Color iconColor, double barHeight, + double sigmaX, + double sigmaY, ) { if (!betterPlayerController!.controlsEnabled) { return const SizedBox(); @@ -171,12 +195,14 @@ class _BetterPlayerCupertinoControlsState borderRadius: BorderRadius.circular(10.0), child: BackdropFilter( filter: ui.ImageFilter.blur( - sigmaX: 10.0, - sigmaY: 10.0, + sigmaX: sigmaX, + sigmaY: sigmaY, ), child: Container( height: barHeight, - color: backgroundColor, + decoration: BoxDecoration( + color: backgroundColor.withOpacity(0.5), + ), child: _betterPlayerController!.isLiveStream() ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -240,7 +266,10 @@ class _BetterPlayerCupertinoControlsState Color backgroundColor, Color iconColor, double barHeight, + double iconSize, double buttonPadding, + double sigmaX, + double sigmaY, ) { return GestureDetector( onTap: _onExpandCollapse, @@ -250,20 +279,22 @@ class _BetterPlayerCupertinoControlsState child: ClipRRect( borderRadius: BorderRadius.circular(10), child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10), + filter: ui.ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY), child: Container( height: barHeight, - padding: EdgeInsets.only( - left: buttonPadding, - right: buttonPadding, + padding: EdgeInsets.symmetric( + horizontal: buttonPadding, + ), + decoration: BoxDecoration( + color: backgroundColor.withOpacity(0.5), ), - color: backgroundColor, child: Center( child: Icon( _betterPlayerController!.isFullScreen ? _controlsConfiguration.fullscreenDisableIcon : _controlsConfiguration.fullscreenEnableIcon, color: iconColor, + size: iconSize, ), ), ), @@ -307,7 +338,9 @@ class _BetterPlayerCupertinoControlsState Color backgroundColor, Color iconColor, double barHeight, + double iconSize, double buttonPadding, + double sigmaX, ) { return GestureDetector( onTap: () { @@ -319,9 +352,11 @@ class _BetterPlayerCupertinoControlsState child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10.0), + filter: ui.ImageFilter.blur(sigmaX: sigmaX), child: Container( - color: backgroundColor, + decoration: BoxDecoration( + color: backgroundColor.withOpacity(0.5), + ), child: Container( height: barHeight, padding: EdgeInsets.symmetric( @@ -330,6 +365,7 @@ class _BetterPlayerCupertinoControlsState child: Icon( _controlsConfiguration.overflowMenuIcon, color: iconColor, + size: iconSize, ), ), ), @@ -344,7 +380,9 @@ class _BetterPlayerCupertinoControlsState Color backgroundColor, Color iconColor, double barHeight, + double iconSize, double buttonPadding, + double sigmaX, ) { return GestureDetector( onTap: () { @@ -363,9 +401,11 @@ class _BetterPlayerCupertinoControlsState child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10.0), + filter: ui.ImageFilter.blur(sigmaX: sigmaX), child: Container( - color: backgroundColor, + decoration: BoxDecoration( + color: backgroundColor.withOpacity(0.5), + ), child: Container( height: barHeight, padding: EdgeInsets.symmetric( @@ -376,6 +416,7 @@ class _BetterPlayerCupertinoControlsState ? _controlsConfiguration.muteIcon : _controlsConfiguration.unMuteIcon, color: iconColor, + size: iconSize, ), ), ), @@ -401,6 +442,7 @@ class _BetterPlayerCupertinoControlsState ? _controlsConfiguration.pauseIcon : _controlsConfiguration.playIcon, color: iconColor, + size: barHeight * 0.6, ), ), ); @@ -444,10 +486,13 @@ class _BetterPlayerCupertinoControlsState height: barHeight, color: Colors.transparent, margin: const EdgeInsets.only(left: 10.0), - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), child: Icon( _controlsConfiguration.skipBackIcon, color: iconColor, + size: barHeight * 0.4, ), ), ); @@ -464,6 +509,7 @@ class _BetterPlayerCupertinoControlsState child: Icon( _controlsConfiguration.skipForwardIcon, color: iconColor, + size: barHeight * 0.4, ), ), ); @@ -472,13 +518,16 @@ class _BetterPlayerCupertinoControlsState Widget _buildTopBar( Color backgroundColor, Color iconColor, - double barHeight, + double topBarHeight, double buttonPadding, + double sigmaX, + double sigmaY, ) { if (!betterPlayerController!.controlsEnabled) { return const SizedBox(); } - + final barHeight = topBarHeight * 0.8; + final iconSize = topBarHeight * 0.4; return Container( height: barHeight, margin: EdgeInsets.only( @@ -490,27 +539,55 @@ class _BetterPlayerCupertinoControlsState children: [ if (_controlsConfiguration.enableFullscreen) _buildExpandButton( - backgroundColor, iconColor, barHeight, buttonPadding) + backgroundColor, + iconColor, + barHeight, + iconSize, + buttonPadding, + sigmaX, + sigmaY, + ) else const SizedBox(), + const SizedBox( + width: 4, + ), if (_controlsConfiguration.enablePip) _buildPipButton( - backgroundColor, iconColor, barHeight, buttonPadding) + backgroundColor, + iconColor, + barHeight, + iconSize, + buttonPadding, + sigmaX, + ) else const SizedBox(), - Expanded(child: Container()), + const Spacer(), if (_controlsConfiguration.enableMute) - _buildMuteButton(_controller, backgroundColor, iconColor, barHeight, - buttonPadding) + _buildMuteButton( + _controller, + backgroundColor, + iconColor, + barHeight, + iconSize, + buttonPadding, + sigmaX, + ) else const SizedBox(), + const SizedBox( + width: 4, + ), if (_controlsConfiguration.enableOverflowMenu) _buildMoreButton( _controller, backgroundColor, iconColor, barHeight, + iconSize, buttonPadding, + sigmaX, ) else const SizedBox(), @@ -706,7 +783,7 @@ class _BetterPlayerCupertinoControlsState mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.warning, + CupertinoIcons.exclamationmark_triangle, color: _controlsConfiguration.iconsColor, size: 42, ), @@ -742,7 +819,7 @@ class _BetterPlayerCupertinoControlsState } Widget _buildPipButton(Color backgroundColor, Color iconColor, - double barHeight, double buttonPadding) { + double barHeight, double iconSize, double buttonPadding, double sigmaX) { return FutureBuilder( future: _betterPlayerController!.isPictureInPictureSupported(), builder: (context, snapshot) { @@ -760,18 +837,21 @@ class _BetterPlayerCupertinoControlsState child: ClipRRect( borderRadius: BorderRadius.circular(10), child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 10), + filter: ui.ImageFilter.blur(sigmaX: sigmaX), child: Container( height: barHeight, padding: EdgeInsets.only( left: buttonPadding, right: buttonPadding, ), - color: backgroundColor, + decoration: BoxDecoration( + color: backgroundColor.withOpacity(0.5), + ), child: Center( child: Icon( _controlsConfiguration.pipMenuIcon, color: iconColor, + size: iconSize, ), ), ), diff --git a/lib/src/controls/better_player_cupertino_progress_bar.dart b/lib/src/controls/better_player_cupertino_progress_bar.dart index 41c72e190..a0158769c 100644 --- a/lib/src/controls/better_player_cupertino_progress_bar.dart +++ b/lib/src/controls/better_player_cupertino_progress_bar.dart @@ -72,6 +72,9 @@ class _VideoProgressBarState final Duration position = controller!.value.duration! * relative; controller!.seekTo(position); } + if (relative >= 1) { + betterPlayerController!.seekTo(controller!.value.duration!); + } } } diff --git a/lib/src/controls/better_player_material_controls.dart b/lib/src/controls/better_player_material_controls.dart index 2f6a5b76a..2f8493e6b 100644 --- a/lib/src/controls/better_player_material_controls.dart +++ b/lib/src/controls/better_player_material_controls.dart @@ -63,6 +63,11 @@ class _BetterPlayerMaterialControlsState @override Widget build(BuildContext context) { + return buildLTRDirectionality(_buildMainWidget()); + } + + ///Builds main widget of the controls. + Widget _buildMainWidget() { _wasLoading = isLoading(_latestValue); if (_latestValue?.hasError == true) { return Container( @@ -86,7 +91,7 @@ class _BetterPlayerMaterialControlsState BetterPlayerMultipleGestureDetector.of(context)!.onDoubleTap?.call(); } cancelAndRestartTimer(); - _onPlayPause(); + //_onPlayPause(); }, onLongPress: () { if (BetterPlayerMultipleGestureDetector.of(context) != null) { @@ -95,14 +100,22 @@ class _BetterPlayerMaterialControlsState }, child: AbsorbPointer( absorbing: _hideStuff, - child: Column( + child: Stack( + fit: StackFit.expand, + //crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildTopBar(), if (_wasLoading) - Expanded(child: Center(child: _buildLoadingWidget())) + Center(child: _buildLoadingWidget()) else _buildHitArea(), - _buildBottomBar(), + Positioned( + top: 0, + left: 0, + right: 0, + child: _buildTopBar(), + ), + Positioned(bottom: 0, left: 0, right: 0, child: _buildBottomBar()), + _buildNextVideoWidget(), ], ), ), @@ -182,29 +195,29 @@ class _BetterPlayerMaterialControlsState return const SizedBox(); } - return Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - if (_controlsConfiguration.enablePip) - _buildPipButtonWrapperWidget(_hideStuff, _onPlayerHide) - else - const SizedBox(), - if (_controlsConfiguration.enableOverflowMenu) - AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: _controlsConfiguration.controlsHideTime, - onEnd: _onPlayerHide, - child: Container( - height: _controlsConfiguration.controlBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildMoreButton(), - ], - ), - ), - ) - else - const SizedBox() - ]); + return Container( + child: (_controlsConfiguration.enableOverflowMenu) + ? AnimatedOpacity( + opacity: _hideStuff ? 0.0 : 1.0, + duration: _controlsConfiguration.controlsHideTime, + onEnd: _onPlayerHide, + child: Container( + //color: _controlsConfiguration.controlBarColor, + height: _controlsConfiguration.controlBarHeight, + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (_controlsConfiguration.enablePip) + _buildPipButtonWrapperWidget(_hideStuff, _onPlayerHide) + else + const SizedBox(), + _buildMoreButton(), + ], + ), + ), + ) + : const SizedBox()); } Widget _buildPipButton() { @@ -276,34 +289,43 @@ class _BetterPlayerMaterialControlsState duration: _controlsConfiguration.controlsHideTime, onEnd: _onPlayerHide, child: Container( - height: _controlsConfiguration.controlBarHeight, - color: _controlsConfiguration.controlBarColor, - child: Row( - children: [ - if (_controlsConfiguration.enablePlayPause) - _buildPlayPause(_controller!) - else - const SizedBox(), - if (_betterPlayerController!.isLiveStream()) - _buildLiveWidget() - else - _controlsConfiguration.enableProgressText - ? _buildPosition() - : const SizedBox(), + height: _controlsConfiguration.controlBarHeight + 20.0, + //color: _controlsConfiguration.controlBarColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 75, + child: Row( + children: [ + if (_controlsConfiguration.enablePlayPause) + _buildPlayPause(_controller!) + else + const SizedBox(), + if (_betterPlayerController!.isLiveStream()) + _buildLiveWidget() + else + _controlsConfiguration.enableProgressText + ? Expanded(child: _buildPosition()) + : const SizedBox(), + const Spacer(), + if (_controlsConfiguration.enableMute) + _buildMuteButton(_controller) + else + const SizedBox(), + if (_controlsConfiguration.enableFullscreen) + _buildExpandButton() + else + const SizedBox(), + ], + ), + ), if (_betterPlayerController!.isLiveStream()) const SizedBox() else _controlsConfiguration.enableProgressBar ? _buildProgressBar() : const SizedBox(), - if (_controlsConfiguration.enableMute) - _buildMuteButton(_controller) - else - const SizedBox(), - if (_controlsConfiguration.enableFullscreen) - _buildExpandButton() - else - const SizedBox(), ], ), ), @@ -311,13 +333,11 @@ class _BetterPlayerMaterialControlsState } Widget _buildLiveWidget() { - return Expanded( - child: Text( - _betterPlayerController!.translations.controlsLive, - style: TextStyle( - color: _controlsConfiguration.liveTextColor, - fontWeight: FontWeight.bold), - ), + return Text( + _betterPlayerController!.translations.controlsLive, + style: TextStyle( + color: _controlsConfiguration.liveTextColor, + fontWeight: FontWeight.bold), ); } @@ -348,36 +368,30 @@ class _BetterPlayerMaterialControlsState if (!betterPlayerController!.controlsEnabled) { return const SizedBox(); } - return Expanded( - child: Container( - color: Colors.transparent, - child: Center( - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: _controlsConfiguration.controlsHideTime, - child: Stack( - children: [ - _buildMiddleRow(), - _buildNextVideoWidget(), - ], - ), - ), + return Container( + child: Center( + child: AnimatedOpacity( + opacity: _hideStuff ? 0.0 : 1.0, + duration: _controlsConfiguration.controlsHideTime, + child: _buildMiddleRow(), ), ), ); } Widget _buildMiddleRow() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + return Container( + color: Colors.black54, //_controlsConfiguration.controlBarColor, + width: double.infinity, + height: double.infinity, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (_controlsConfiguration.enableSkips) _buildSkipButton() else const SizedBox(), - _buildReplayButton(), + _buildReplayButton(_controller!), if (_controlsConfiguration.enableSkips) _buildForwardButton() else @@ -389,18 +403,21 @@ class _BetterPlayerMaterialControlsState Widget _buildHitAreaClickableButton( {Widget? icon, required void Function() onClicked}) { - return BetterPlayerMaterialClickableWidget( - onTap: onClicked, - child: Align( - child: Container( - decoration: BoxDecoration( - color: _controlsConfiguration.controlBarColor, - borderRadius: BorderRadius.circular(48), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Stack( - children: [icon!], + return Container( + constraints: const BoxConstraints(maxHeight: 80.0, maxWidth: 80.0), + child: BetterPlayerMaterialClickableWidget( + onTap: onClicked, + child: Align( + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(48), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Stack( + children: [icon!], + ), ), ), ), @@ -412,7 +429,7 @@ class _BetterPlayerMaterialControlsState return _buildHitAreaClickableButton( icon: Icon( _controlsConfiguration.skipBackIcon, - size: 32, + size: 24, color: _controlsConfiguration.iconsColor, ), onClicked: skipBack, @@ -423,40 +440,48 @@ class _BetterPlayerMaterialControlsState return _buildHitAreaClickableButton( icon: Icon( _controlsConfiguration.skipForwardIcon, - size: 32, + size: 24, color: _controlsConfiguration.iconsColor, ), onClicked: skipForward, ); } - Widget _buildReplayButton() { + Widget _buildReplayButton(VideoPlayerController controller) { final bool isFinished = isVideoFinished(_latestValue); - if (!isFinished) { - return const SizedBox(); - } - return _buildHitAreaClickableButton( - icon: Icon( - Icons.replay, - size: 32, - color: _controlsConfiguration.iconsColor, - ), + icon: isFinished + ? Icon( + Icons.replay, + size: 42, + color: _controlsConfiguration.iconsColor, + ) + : Icon( + controller.value.isPlaying + ? _controlsConfiguration.pauseIcon + : _controlsConfiguration.playIcon, + size: 42, + color: _controlsConfiguration.iconsColor, + ), onClicked: () { - if (_latestValue != null && _latestValue!.isPlaying) { - if (_displayTapped) { + if (isFinished) { + if (_latestValue != null && _latestValue!.isPlaying) { + if (_displayTapped) { + setState(() { + _hideStuff = true; + }); + } else { + cancelAndRestartTimer(); + } + } else { + _onPlayPause(); + setState(() { _hideStuff = true; }); - } else { - cancelAndRestartTimer(); } } else { _onPlayPause(); - - setState(() { - _hideStuff = true; - }); } }, ); @@ -475,15 +500,17 @@ class _BetterPlayerMaterialControlsState child: Align( alignment: Alignment.bottomRight, child: Container( - margin: const EdgeInsets.only(bottom: 4, right: 24), + margin: EdgeInsets.only( + bottom: _controlsConfiguration.controlBarHeight + 20, + right: 24), decoration: BoxDecoration( color: _controlsConfiguration.controlBarColor, - borderRadius: BorderRadius.circular(32), + borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(12), child: Text( - "${_betterPlayerController!.translations.controlsNextVideoIn} $time ...", + "${_betterPlayerController!.translations.controlsNextVideoIn} $time...", style: const TextStyle(color: Colors.white), ), ), @@ -533,7 +560,7 @@ class _BetterPlayerMaterialControlsState return BetterPlayerMaterialClickableWidget( onTap: _onPlayPause, child: Container( - height: _controlsConfiguration.controlBarHeight, + height: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 12), child: Icon( @@ -554,14 +581,27 @@ class _BetterPlayerMaterialControlsState : Duration.zero; return Padding( - padding: const EdgeInsets.only(right: 24), - child: Text( - '${BetterPlayerUtils.formatDuration(position)} / ${BetterPlayerUtils.formatDuration(duration)}', - style: TextStyle( - fontSize: 14, - color: _controlsConfiguration.textColor, - decoration: TextDecoration.none, - ), + padding: _controlsConfiguration.enablePlayPause + ? const EdgeInsets.only(right: 24) + : const EdgeInsets.symmetric(horizontal: 22), + child: RichText( + text: TextSpan( + text: BetterPlayerUtils.formatDuration(position), + style: TextStyle( + fontSize: 10.0, + color: _controlsConfiguration.textColor, + decoration: TextDecoration.none, + ), + children: [ + TextSpan( + text: ' / ${BetterPlayerUtils.formatDuration(duration)}', + style: const TextStyle( + fontSize: 10.0, + color: Colors.grey, + decoration: TextDecoration.none, + ), + ) + ]), ), ); } @@ -676,26 +716,27 @@ class _BetterPlayerMaterialControlsState Widget _buildProgressBar() { return Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 20), - child: BetterPlayerMaterialVideoProgressBar( - _controller, - _betterPlayerController, - onDragStart: () { - _hideTimer?.cancel(); - }, - onDragEnd: () { - _startHideTimer(); - }, - colors: BetterPlayerProgressColors( - playedColor: _controlsConfiguration.progressBarPlayedColor, - handleColor: _controlsConfiguration.progressBarHandleColor, - bufferedColor: _controlsConfiguration.progressBarBufferedColor, - backgroundColor: - _controlsConfiguration.progressBarBackgroundColor), - ), - ), - ); + flex: 40, + child: Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: BetterPlayerMaterialVideoProgressBar( + _controller, + _betterPlayerController, + onDragStart: () { + _hideTimer?.cancel(); + }, + onDragEnd: () { + _startHideTimer(); + }, + colors: BetterPlayerProgressColors( + playedColor: _controlsConfiguration.progressBarPlayedColor, + handleColor: _controlsConfiguration.progressBarHandleColor, + bufferedColor: _controlsConfiguration.progressBarBufferedColor, + backgroundColor: + _controlsConfiguration.progressBarBackgroundColor), + ), + )); } void _onPlayerHide() { @@ -705,7 +746,10 @@ class _BetterPlayerMaterialControlsState Widget? _buildLoadingWidget() { if (_controlsConfiguration.loadingWidget != null) { - return _controlsConfiguration.loadingWidget; + return Container( + color: _controlsConfiguration.controlBarColor, + child: _controlsConfiguration.loadingWidget, + ); } return CircularProgressIndicator( diff --git a/lib/src/controls/better_player_material_progress_bar.dart b/lib/src/controls/better_player_material_progress_bar.dart index f035fd1d9..3f0012462 100644 --- a/lib/src/controls/better_player_material_progress_bar.dart +++ b/lib/src/controls/better_player_material_progress_bar.dart @@ -72,6 +72,9 @@ class _VideoProgressBarState final Duration position = controller!.value.duration! * relative; betterPlayerController!.seekTo(position); } + if (relative >= 1) { + betterPlayerController!.seekTo(controller!.value.duration!); + } } } @@ -168,13 +171,22 @@ class _ProgressBarPainter extends CustomPainter { if (!value.initialized) { return; } - final double playedPartPercent = + double playedPartPercent = value.position.inMilliseconds / value.duration!.inMilliseconds; + if (playedPartPercent.isNaN) { + playedPartPercent = 0; + } final double playedPart = playedPartPercent > 1 ? size.width : playedPartPercent * size.width; for (final DurationRange range in value.buffered) { - final double start = range.startFraction(value.duration!) * size.width; - final double end = range.endFraction(value.duration!) * size.width; + double start = range.startFraction(value.duration!) * size.width; + if (start.isNaN) { + start = 0; + } + double end = range.endFraction(value.duration!) * size.width; + if (end.isNaN) { + end = 0; + } canvas.drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index 012b6da0b..1528f8aaf 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -213,6 +213,9 @@ class BetterPlayerController { ///List of loaded ASMS segments final List _asmsSegmentsLoaded = []; + ///Currently displayed [BetterPlayerSubtitle]. + BetterPlayerSubtitle? renderedSubtitle; + BetterPlayerController( this.betterPlayerConfiguration, { this.betterPlayerPlaylistConfiguration, @@ -441,33 +444,33 @@ class BetterPlayerController { switch (betterPlayerDataSource.type) { case BetterPlayerDataSourceType.network: await videoPlayerController?.setNetworkDataSource( - betterPlayerDataSource.url, - headers: _getHeaders(), - useCache: - _betterPlayerDataSource!.cacheConfiguration?.useCache ?? false, - maxCacheSize: - _betterPlayerDataSource!.cacheConfiguration?.maxCacheSize ?? 0, - maxCacheFileSize: - _betterPlayerDataSource!.cacheConfiguration?.maxCacheFileSize ?? - 0, - cacheKey: _betterPlayerDataSource?.cacheConfiguration?.key, - showNotification: _betterPlayerDataSource - ?.notificationConfiguration?.showNotification, - title: _betterPlayerDataSource?.notificationConfiguration?.title, - author: _betterPlayerDataSource?.notificationConfiguration?.author, - imageUrl: - _betterPlayerDataSource?.notificationConfiguration?.imageUrl, - notificationChannelName: _betterPlayerDataSource - ?.notificationConfiguration?.notificationChannelName, - overriddenDuration: _betterPlayerDataSource!.overriddenDuration, - formatHint: _getVideoFormat(_betterPlayerDataSource!.videoFormat), - licenseUrl: _betterPlayerDataSource?.drmConfiguration?.licenseUrl, - certificateUrl: - _betterPlayerDataSource?.drmConfiguration?.certificateUrl, - drmHeaders: _betterPlayerDataSource?.drmConfiguration?.headers, - activityName: - _betterPlayerDataSource?.notificationConfiguration?.activityName, - ); + betterPlayerDataSource.url, + headers: _getHeaders(), + useCache: + _betterPlayerDataSource!.cacheConfiguration?.useCache ?? false, + maxCacheSize: + _betterPlayerDataSource!.cacheConfiguration?.maxCacheSize ?? 0, + maxCacheFileSize: + _betterPlayerDataSource!.cacheConfiguration?.maxCacheFileSize ?? + 0, + cacheKey: _betterPlayerDataSource?.cacheConfiguration?.key, + showNotification: _betterPlayerDataSource + ?.notificationConfiguration?.showNotification, + title: _betterPlayerDataSource?.notificationConfiguration?.title, + author: _betterPlayerDataSource?.notificationConfiguration?.author, + imageUrl: + _betterPlayerDataSource?.notificationConfiguration?.imageUrl, + notificationChannelName: _betterPlayerDataSource + ?.notificationConfiguration?.notificationChannelName, + overriddenDuration: _betterPlayerDataSource!.overriddenDuration, + formatHint: _getVideoFormat(_betterPlayerDataSource!.videoFormat), + licenseUrl: _betterPlayerDataSource?.drmConfiguration?.licenseUrl, + certificateUrl: + _betterPlayerDataSource?.drmConfiguration?.certificateUrl, + drmHeaders: _betterPlayerDataSource?.drmConfiguration?.headers, + activityName: _betterPlayerDataSource + ?.notificationConfiguration?.activityName, + clearKey: _betterPlayerDataSource?.drmConfiguration?.clearKey); break; case BetterPlayerDataSourceType.file: @@ -491,7 +494,8 @@ class BetterPlayerController { ?.notificationConfiguration?.notificationChannelName, overriddenDuration: _betterPlayerDataSource!.overriddenDuration, activityName: _betterPlayerDataSource - ?.notificationConfiguration?.activityName); + ?.notificationConfiguration?.activityName, + clearKey: _betterPlayerDataSource?.drmConfiguration?.clearKey); break; case BetterPlayerDataSourceType.memory: final file = await _createFile(_betterPlayerDataSource!.bytes!, @@ -510,7 +514,8 @@ class BetterPlayerController { ?.notificationConfiguration?.notificationChannelName, overriddenDuration: _betterPlayerDataSource!.overriddenDuration, activityName: _betterPlayerDataSource - ?.notificationConfiguration?.activityName); + ?.notificationConfiguration?.activityName, + clearKey: _betterPlayerDataSource?.drmConfiguration?.clearKey); _tempFiles.add(file); } else { throw ArgumentError("Couldn't create file from memory."); @@ -1224,7 +1229,9 @@ class BetterPlayerController { /// Add controller internal event. void _postControllerEvent(BetterPlayerControllerEvent event) { - _controllerEventStreamController.add(event); + if (!_controllerEventStreamController.isClosed) { + _controllerEventStreamController.add(event); + } } ///Dispose BetterPlayerController. When [forceDispose] parameter is true, then diff --git a/lib/src/list/better_player_list_video_player_controller.dart b/lib/src/list/better_player_list_video_player_controller.dart index 278701d75..efd83ecaf 100644 --- a/lib/src/list/better_player_list_video_player_controller.dart +++ b/lib/src/list/better_player_list_video_player_controller.dart @@ -26,4 +26,8 @@ class BetterPlayerListVideoPlayerController { BetterPlayerController? betterPlayerController) { _betterPlayerController = betterPlayerController; } + + void setMixWithOthers(bool mixWithOthers) { + _betterPlayerController?.setMixWithOthers(mixWithOthers); + } } diff --git a/lib/src/subtitles/better_player_subtitle.dart b/lib/src/subtitles/better_player_subtitle.dart index f157e4297..c938fe6fe 100644 --- a/lib/src/subtitles/better_player_subtitle.dart +++ b/lib/src/subtitles/better_player_subtitle.dart @@ -111,4 +111,9 @@ class BetterPlayerSubtitle { return const Duration(); } } + + @override + String toString() { + return 'BetterPlayerSubtitle{index: $index, start: $start, end: $end, texts: $texts, type: $type}'; + } } diff --git a/lib/src/subtitles/better_player_subtitles_drawer.dart b/lib/src/subtitles/better_player_subtitles_drawer.dart index 238772f56..57122876d 100644 --- a/lib/src/subtitles/better_player_subtitles_drawer.dart +++ b/lib/src/subtitles/better_player_subtitles_drawer.dart @@ -101,7 +101,9 @@ class _BetterPlayerSubtitlesDrawerState @override Widget build(BuildContext context) { - final List subtitles = _getSubtitlesAtCurrentPosition()!; + final BetterPlayerSubtitle? subtitle = _getSubtitleAtCurrentPosition(); + widget.betterPlayerController.renderedSubtitle = subtitle; + final List subtitles = subtitle?.texts ?? []; final List textWidgets = subtitles.map((text) => _buildSubtitleTextWidget(text)).toList(); @@ -123,19 +125,19 @@ class _BetterPlayerSubtitlesDrawerState ); } - List? _getSubtitlesAtCurrentPosition() { + BetterPlayerSubtitle? _getSubtitleAtCurrentPosition() { if (_latestValue == null) { - return []; + return null; } final Duration position = _latestValue!.position; for (final BetterPlayerSubtitle subtitle in widget.betterPlayerController.subtitlesLines) { if (subtitle.start! <= position && subtitle.end! >= position) { - return subtitle.texts; + return subtitle; } } - return []; + return null; } Widget _buildSubtitleTextWidget(String subtitleText) { diff --git a/lib/src/video_player/method_channel_video_player.dart b/lib/src/video_player/method_channel_video_player.dart index 11306a77d..c791d947e 100644 --- a/lib/src/video_player/method_channel_video_player.dart +++ b/lib/src/video_player/method_channel_video_player.dart @@ -101,6 +101,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { 'certificateUrl': dataSource.certificateUrl, 'drmHeaders': dataSource.drmHeaders, 'activityName': dataSource.activityName, + 'clearKey': dataSource.clearKey }; break; case DataSourceType.file: @@ -116,7 +117,8 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { 'imageUrl': dataSource.imageUrl, 'notificationChannelName': dataSource.notificationChannelName, 'overriddenDuration': dataSource.overriddenDuration?.inMilliseconds, - 'activityName': dataSource.activityName + 'activityName': dataSource.activityName, + 'clearKey': dataSource.clearKey }; break; } diff --git a/lib/src/video_player/video_player.dart b/lib/src/video_player/video_player.dart index 866546fe8..7d686a429 100644 --- a/lib/src/video_player/video_player.dart +++ b/lib/src/video_player/video_player.dart @@ -322,46 +322,46 @@ class VideoPlayerController extends ValueNotifier { /// null. /// **Android only**: The [formatHint] option allows the caller to override /// the video format detection code. - Future setNetworkDataSource( - String dataSource, { - VideoFormat? formatHint, - Map? headers, - bool useCache = false, - int? maxCacheSize, - int? maxCacheFileSize, - String? cacheKey, - bool? showNotification, - String? title, - String? author, - String? imageUrl, - String? notificationChannelName, - Duration? overriddenDuration, - String? licenseUrl, - String? certificateUrl, - Map? drmHeaders, - String? activityName, - }) { + /// ClearKey DRM only supported on Android. + Future setNetworkDataSource(String dataSource, + {VideoFormat? formatHint, + Map? headers, + bool useCache = false, + int? maxCacheSize, + int? maxCacheFileSize, + String? cacheKey, + bool? showNotification, + String? title, + String? author, + String? imageUrl, + String? notificationChannelName, + Duration? overriddenDuration, + String? licenseUrl, + String? certificateUrl, + Map? drmHeaders, + String? activityName, + String? clearKey}) { return _setDataSource( DataSource( - sourceType: DataSourceType.network, - uri: dataSource, - formatHint: formatHint, - headers: headers, - useCache: useCache, - maxCacheSize: maxCacheSize, - maxCacheFileSize: maxCacheFileSize, - cacheKey: cacheKey, - showNotification: showNotification, - title: title, - author: author, - imageUrl: imageUrl, - notificationChannelName: notificationChannelName, - overriddenDuration: overriddenDuration, - licenseUrl: licenseUrl, - certificateUrl: certificateUrl, - drmHeaders: drmHeaders, - activityName: activityName, - ), + sourceType: DataSourceType.network, + uri: dataSource, + formatHint: formatHint, + headers: headers, + useCache: useCache, + maxCacheSize: maxCacheSize, + maxCacheFileSize: maxCacheFileSize, + cacheKey: cacheKey, + showNotification: showNotification, + title: title, + author: author, + imageUrl: imageUrl, + notificationChannelName: notificationChannelName, + overriddenDuration: overriddenDuration, + licenseUrl: licenseUrl, + certificateUrl: certificateUrl, + drmHeaders: drmHeaders, + activityName: activityName, + clearKey: clearKey), ); } @@ -369,16 +369,15 @@ class VideoPlayerController extends ValueNotifier { /// /// This will load the file from the file-URI given by: /// `'file://${file.path}'`. - Future setFileDataSource( - File file, { - bool? showNotification, - String? title, - String? author, - String? imageUrl, - String? notificationChannelName, - Duration? overriddenDuration, - String? activityName, - }) { + Future setFileDataSource(File file, + {bool? showNotification, + String? title, + String? author, + String? imageUrl, + String? notificationChannelName, + Duration? overriddenDuration, + String? activityName, + String? clearKey}) { return _setDataSource( DataSource( sourceType: DataSourceType.file, @@ -389,7 +388,8 @@ class VideoPlayerController extends ValueNotifier { imageUrl: imageUrl, notificationChannelName: notificationChannelName, overriddenDuration: overriddenDuration, - activityName: activityName), + activityName: activityName, + clearKey: clearKey), ); } diff --git a/lib/src/video_player/video_player_platform_interface.dart b/lib/src/video_player/video_player_platform_interface.dart index 60b628e34..bcbb510bb 100644 --- a/lib/src/video_player/video_player_platform_interface.dart +++ b/lib/src/video_player/video_player_platform_interface.dart @@ -209,28 +209,29 @@ class DataSource { /// The [package] argument must be non-null when the asset comes from a /// package and null otherwise. /// - DataSource({ - required this.sourceType, - this.uri, - this.formatHint, - this.asset, - this.package, - this.headers, - this.useCache = false, - this.maxCacheSize = _maxCacheSize, - this.maxCacheFileSize = _maxCacheFileSize, - this.cacheKey, - this.showNotification = false, - this.title, - this.author, - this.imageUrl, - this.notificationChannelName, - this.overriddenDuration, - this.licenseUrl, - this.certificateUrl, - this.drmHeaders, - this.activityName, - }) : assert(uri == null || asset == null); + DataSource( + {required this.sourceType, + this.uri, + this.formatHint, + this.asset, + this.package, + this.headers, + this.useCache = false, + this.maxCacheSize = _maxCacheSize, + this.maxCacheFileSize = _maxCacheFileSize, + this.cacheKey, + this.showNotification = false, + this.title, + this.author, + this.imageUrl, + this.notificationChannelName, + this.overriddenDuration, + this.licenseUrl, + this.certificateUrl, + this.drmHeaders, + this.activityName, + this.clearKey}) + : assert(uri == null || asset == null); /// Describes the type of data source this [VideoPlayerController] /// is constructed with. @@ -304,6 +305,8 @@ class DataSource { final String? activityName; + final String? clearKey; + /// Key to compare DataSource String get key { String? result = ""; diff --git a/pubspec.lock b/pubspec.lock index 45f28455e..2be5ac9a2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.7.0" boolean_selector: dependency: transitive description: @@ -28,7 +28,7 @@ packages: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -146,7 +146,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.0" path: dependency: transitive description: @@ -270,7 +270,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 46e590db1..225b9ece6 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.72 +version: 0.0.73 authors: - Jakub Homlala homepage: https://github.com/jhomlala/betterplayer