diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c647e936..cff915c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.0.64 +* Added Turkish translations (by https://github.com/smurat). +* Video fit fixes (by https://github.com/themadmrj). +* Fixed speed iOS issue. +* Fixed Android's notification image OOM issue. +* Fixed 0 second delay issue in playlist. +* Fixed drmHeaders to be sent in headers rather than request body (by https://github.com/FlutterSu) +* Added preCache, stopPreCache method in BetterPlayerController (coauthored with: https://github.com/themadmrj) +* [BREAKING_CHANGE] clearCache method doesn't require to setup data source in order to use. + ## 0.0.63 * Fixed pause method in dispose. * Added clearCache method in BetterPlayerController. diff --git a/README.md b/README.md index c5fec9032..39c169512 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ This plugin is based on [Chewie](https://github.com/brianegan/chewie). Chewie is ```yaml dependencies: - better_player: ^0.0.63 + better_player: ^0.0.64 ``` 2. Install it @@ -1061,6 +1061,22 @@ Default value is false. ### More documentation https://pub.dev/documentation/better_player/latest/better_player/better_player-library.html +### Cache +Clear all cached data: + +```dart +betterPlayerController.clearCache(); +``` +Start pre cache before playing video (android only): +```dart +betterPlayerController.preCache(_betterPlayerDataSource); +```dart + +Stop running pre cache (android only): +```dart +betterPlayerController.stopPreCache(_betterPlayerDataSource); +```dart + diff --git a/android/build.gradle b/android/build.gradle index cfddca7fa..2521888bc 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -50,6 +50,7 @@ android { implementation "android.arch.lifecycle:common-java8:1.1.1" implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation "androidx.work:work-runtime:2.5.0" } } 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 3b3f398a3..a47c8cd58 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayer.java @@ -2,6 +2,8 @@ import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; +import static com.jhomlala.better_player.DataSourceUtils.getDataSourceFactory; +import static com.jhomlala.better_player.DataSourceUtils.getUserAgent; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -61,13 +63,15 @@ import androidx.annotation.Nullable; import androidx.media.session.MediaButtonReceiver; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.view.TextureRegistry; import java.io.File; -import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; @@ -87,9 +91,8 @@ final class BetterPlayer { private static final String FORMAT_HLS = "hls"; private static final String FORMAT_OTHER = "other"; private static final String DEFAULT_NOTIFICATION_CHANNEL = "BETTER_PLAYER_NOTIFICATION"; - private static final String USER_AGENT = "User-Agent"; - private static final String USER_AGENT_PROPERTY = "http.agent"; private static final int NOTIFICATION_ID = 20772077; + private static final int DEFAULT_NOTIFICATION_IMAGE_SIZE_PX = 256; private final SimpleExoPlayer exoPlayer; private final TextureRegistry.SurfaceTextureEntry textureEntry; @@ -132,13 +135,7 @@ void setDataSource( Uri uri = Uri.parse(dataSource); DataSource.Factory dataSourceFactory; - String userAgent = System.getProperty(USER_AGENT_PROPERTY); - if (headers != null && headers.containsKey(USER_AGENT)) { - String userAgentHeader = headers.get(USER_AGENT); - if (userAgentHeader != null) { - userAgent = userAgentHeader; - } - } + String userAgent = getUserAgent(headers); if (licenseUrl != null && !licenseUrl.isEmpty()) { HttpMediaDrmCallback httpMediaDrmCallback = @@ -175,16 +172,8 @@ void setDataSource( drmSessionManager = null; } - if (isHTTP(uri)) { - dataSourceFactory = new DefaultHttpDataSource.Factory() - .setUserAgent(userAgent) - .setAllowCrossProtocolRedirects(true) - .setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS) - .setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS); - - if (headers != null) { - ((DefaultHttpDataSource.Factory) dataSourceFactory).setDefaultRequestProperties(headers); - } + if (DataSourceUtils.isHTTP(uri)) { + dataSourceFactory = getDataSourceFactory(userAgent, headers); if (useCache && maxCacheSize > 0 && maxCacheFileSize > 0) { dataSourceFactory = @@ -410,36 +399,72 @@ public void disposeRemoteNotifications() { bitmap = null; } - private static Bitmap getBitmapFromInternalURL(String src) { + private Bitmap getBitmapFromInternalURL(String src) { try { + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + options.inSampleSize = calculateBitmapInSmapleSize(options, + DEFAULT_NOTIFICATION_IMAGE_SIZE_PX, + DEFAULT_NOTIFICATION_IMAGE_SIZE_PX); + options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(src); } catch (Exception exception) { + Log.e(TAG, "Failed to get bitmap from internal url: " + src); return null; } } - private static Bitmap getBitmapFromExternalURL(String src) { + + private Bitmap getBitmapFromExternalURL(String src) { + InputStream inputStream = null; try { URL url = new URL(src); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.connect(); - InputStream input = connection.getInputStream(); - return BitmapFactory.decodeStream(input); - } catch (IOException exception) { + inputStream = connection.getInputStream(); + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + inputStream.close(); + connection = (HttpURLConnection) url.openConnection(); + inputStream = connection.getInputStream(); + options.inSampleSize = calculateBitmapInSmapleSize( + options, DEFAULT_NOTIFICATION_IMAGE_SIZE_PX, DEFAULT_NOTIFICATION_IMAGE_SIZE_PX); + options.inJustDecodeBounds = false; + return BitmapFactory.decodeStream(inputStream, null, options); + + } catch (Exception exception) { + Log.e(TAG, "Failed to get bitmap from external url: " + src); return null; + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (Exception exception) { + Log.e(TAG, "Failed to close bitmap input stream/"); + } } } + private int calculateBitmapInSmapleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; - private static boolean isHTTP(Uri uri) { - if (uri == null || uri.getScheme() == null) { - return false; + if (height > reqHeight || width > reqWidth) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + while ((halfHeight / inSampleSize) >= reqHeight + && (halfWidth / inSampleSize) >= reqWidth) { + inSampleSize *= 2; + } } - String scheme = uri.getScheme(); - return scheme.equals("http") || scheme.equals("https"); + return inSampleSize; } + private MediaSource buildMediaSource( Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { int type; @@ -801,15 +826,44 @@ public void setMixWithOthers(Boolean mixWithOthers) { //Clear cache without accessing BetterPlayerCache. @SuppressWarnings("ResultOfMethodCallIgnored") - public void clearCache(Context context) { + public static void clearCache(Context context, Result result) { try { File file = context.getCacheDir(); if (file != null) { file.delete(); } + result.success(null); } catch (Exception exception) { - Log.e("Cache", exception.toString()); + Log.e(TAG, exception.toString()); + result.error("", "", ""); + } + } + + //Start pre cache of video. Invoke work manager job and start caching in background. + static void preCache(Context context, String dataSource, long preCacheSize, + long maxCacheSize, long maxCacheFileSize, Map headers, + Result result) { + Data.Builder dataBuilder = new Data.Builder() + .putString(BetterPlayerPlugin.URL_PARAMETER, dataSource) + .putLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, preCacheSize) + .putLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, maxCacheSize) + .putLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, maxCacheFileSize); + for (String headerKey : headers.keySet()) { + dataBuilder.putString(BetterPlayerPlugin.HEADER_PARAMETER + headerKey, headers.get(headerKey)); } + + OneTimeWorkRequest cacheWorkRequest = new OneTimeWorkRequest.Builder(CacheWorker.class) + .addTag(dataSource) + .setInputData(dataBuilder.build()).build(); + WorkManager.getInstance(context).enqueue(cacheWorkRequest); + result.success(null); + } + + //Stop pre cache of video with given url. If there's no work manager job for given url, then + //it will be ignored. + static void stopPreCache(Context context, String url, Result result) { + WorkManager.getInstance(context).cancelAllWorkByTag(url); + result.success(null); } void dispose() { 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 ba335ab69..c31f2ac04 100644 --- a/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java +++ b/android/src/main/java/com/jhomlala/better_player/BetterPlayerPlugin.java @@ -42,8 +42,8 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC private static final String KEY_PARAMETER = "key"; private static final String HEADERS_PARAMETER = "headers"; private static final String USE_CACHE_PARAMETER = "useCache"; - private static final String MAX_CACHE_SIZE_PARAMETER = "maxCacheSize"; - private static final String MAX_CACHE_FILE_SIZE_PARAMETER = "maxCacheFileSize"; + + private static final String ASSET_PARAMETER = "asset"; private static final String PACKAGE_PARAMETER = "package"; private static final String URI_PARAMETER = "uri"; @@ -67,6 +67,12 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC private static final String LICENSE_URL_PARAMETER = "licenseUrl"; private static final String DRM_HEADERS_PARAMETER = "drmHeaders"; 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"; + public static final String MAX_CACHE_SIZE_PARAMETER = "maxCacheSize"; + public static final String MAX_CACHE_FILE_SIZE_PARAMETER = "maxCacheFileSize"; + public static final String HEADER_PARAMETER = "header_"; + private static final String INIT_METHOD = "init"; private static final String CREATE_METHOD = "create"; @@ -87,6 +93,8 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC private static final String SET_MIX_WITH_OTHERS_METHOD = "setMixWithOthers"; private static final String CLEAR_CACHE_METHOD = "clearCache"; private static final String DISPOSE_METHOD = "dispose"; + private static final String PRE_CACHE_METHOD = "preCache"; + private static final String STOP_PRE_CACHE_METHOD = "stopPreCache"; private final LongSparseArray videoPlayers = new LongSparseArray<>(); private final LongSparseArray> dataSources = new LongSparseArray<>(); @@ -102,6 +110,7 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC public BetterPlayerPlugin() { } + @SuppressWarnings("deprecation") private BetterPlayerPlugin(Registrar registrar) { this.flutterState = new FlutterState( @@ -116,6 +125,7 @@ private BetterPlayerPlugin(Registrar registrar) { /** * Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ + @SuppressWarnings("deprecation") public static void registerWith(Registrar registrar) { final BetterPlayerPlugin plugin = new BetterPlayerPlugin(registrar); @@ -127,6 +137,7 @@ public static void registerWith(Registrar registrar) { } + @SuppressWarnings("deprecation") @Override public void onAttachedToEngine(FlutterPluginBinding binding) { this.flutterState = @@ -193,6 +204,15 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { videoPlayers.put(handle.id(), player); break; } + case PRE_CACHE_METHOD: + preCache(call, result); + break; + case STOP_PRE_CACHE_METHOD: + stopPreCache(call, result); + break; + case CLEAR_CACHE_METHOD: + clearCache(result); + break; default: { long textureId = ((Number) call.argument(TEXTURE_ID_PARAMETER)).longValue(); BetterPlayer player = videoPlayers.get(textureId); @@ -277,9 +297,6 @@ private void onMethodCall(MethodCall call, Result result, long textureId, Better case SET_MIX_WITH_OTHERS_METHOD: player.setMixWithOthers(call.argument(MIX_WITH_OTHERS_PARAMETER)); break; - case CLEAR_CACHE_METHOD: - player.clearCache(flutterState.applicationContext); - break; case DISPOSE_METHOD: dispose(player, textureId); result.success(null); @@ -351,6 +368,50 @@ private void setDataSource(MethodCall call, Result result, BetterPlayer player) } } + /** + * Start pre cache of video. + * + * @param call - invoked method data + * @param result - result which should be updated + */ + private void preCache(MethodCall call, Result result) { + Map dataSource = call.argument(DATA_SOURCE_PARAMETER); + if (dataSource != null) { + Number maxCacheSizeNumber = getParameter(dataSource, MAX_CACHE_SIZE_PARAMETER, 100 * 1024 * 1024); + Number maxCacheFileSizeNumber = getParameter(dataSource, MAX_CACHE_FILE_SIZE_PARAMETER, 10 * 1024 * 1024); + long maxCacheSize = maxCacheSizeNumber.longValue(); + long maxCacheFileSize = maxCacheFileSizeNumber.longValue(); + Number preCacheSizeNumber = getParameter(dataSource, PRE_CACHE_SIZE_PARAMETER, 3 * 1024 * 1024); + long preCacheSize = preCacheSizeNumber.longValue(); + String uri = getParameter(dataSource, URI_PARAMETER, ""); + Map headers = getParameter(dataSource, HEADERS_PARAMETER, new HashMap<>()); + + BetterPlayer.preCache(flutterState.applicationContext, + uri, + preCacheSize, + maxCacheSize, + maxCacheFileSize, + headers, + result + ); + } + } + + /** + * Stop pre cache video process (if exists). + * + * @param call - invoked method data + * @param result - result which should be updated + */ + private void stopPreCache(MethodCall call, Result result) { + String url = call.argument(URL_PARAMETER); + BetterPlayer.stopPreCache(flutterState.applicationContext, url, result); + } + + private void clearCache(Result result) { + BetterPlayer.clearCache(flutterState.applicationContext, result); + } + private Long getTextureId(BetterPlayer betterPlayer) { for (int index = 0; index < videoPlayers.size(); index++) { if (betterPlayer == videoPlayers.valueAt(index)) { diff --git a/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java b/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java index 53f41cd02..5451bbc05 100644 --- a/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java +++ b/android/src/main/java/com/jhomlala/better_player/CacheDataSourceFactory.java @@ -35,7 +35,7 @@ class CacheDataSourceFactory implements DataSource.Factory { @SuppressWarnings("NullableProblems") @Override - public DataSource createDataSource() { + public CacheDataSource createDataSource() { SimpleCache betterPlayerCache = BetterPlayerCache.createCache(context, maxCacheSize); return new CacheDataSource( betterPlayerCache, diff --git a/android/src/main/java/com/jhomlala/better_player/CacheWorker.java b/android/src/main/java/com/jhomlala/better_player/CacheWorker.java new file mode 100644 index 000000000..d66934120 --- /dev/null +++ b/android/src/main/java/com/jhomlala/better_player/CacheWorker.java @@ -0,0 +1,98 @@ +package com.jhomlala.better_player; + +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheWriter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + + +/** + * Cache worker which download part of video and save in cache for future usage. The cache job + * will be executed in work manager. + */ +public class CacheWorker extends Worker { + private static final String TAG = "CacheWorker"; + private Context mContext; + private CacheWriter mCacheWriter; + private int mLastCacheReportIndex = 0; + + public CacheWorker( + @NonNull Context context, + @NonNull WorkerParameters params) { + super(context, params); + this.mContext = context; + } + + @NonNull + @Override + public Result doWork() { + try { + Data data = getInputData(); + String url = data.getString(BetterPlayerPlugin.URL_PARAMETER); + long preCacheSize = data.getLong(BetterPlayerPlugin.PRE_CACHE_SIZE_PARAMETER, 0); + long maxCacheSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_SIZE_PARAMETER, 0); + long maxCacheFileSize = data.getLong(BetterPlayerPlugin.MAX_CACHE_FILE_SIZE_PARAMETER, 0); + Map headers = new HashMap<>(); + for (String key : data.getKeyValueMap().keySet()) { + if (key.contains(BetterPlayerPlugin.HEADER_PARAMETER)) { + String keySplit = key.split(BetterPlayerPlugin.HEADER_PARAMETER)[0]; + headers.put(keySplit, (String) Objects.requireNonNull(data.getKeyValueMap().get(key))); + } + } + + Uri uri = Uri.parse(url); + if (DataSourceUtils.isHTTP(uri)) { + String userAgent = DataSourceUtils.getUserAgent(headers); + DataSource.Factory dataSourceFactory = DataSourceUtils.getDataSourceFactory(userAgent, headers); + DataSpec dataSpec = new DataSpec(uri, 0, preCacheSize); + CacheDataSourceFactory cacheDataSourceFactory = + new CacheDataSourceFactory(mContext, maxCacheSize, maxCacheFileSize, dataSourceFactory); + + mCacheWriter = new CacheWriter( + cacheDataSourceFactory.createDataSource(), + dataSpec, + true, + null, + (long requestLength, long bytesCached, long newBytesCached) -> { + double completedData = ((bytesCached * 100f) / preCacheSize); + if (completedData >= mLastCacheReportIndex * 10) { + mLastCacheReportIndex += 1; + Log.d(TAG, "Completed pre cache of " + url + ": " + (int) completedData + "%"); + } + }); + + mCacheWriter.cache(); + } else { + Log.e(TAG, "Preloading only possible for remote data sources"); + return Result.failure(); + } + } catch (Exception exception) { + Log.e(TAG, exception.toString()); + if (exception instanceof HttpDataSource.HttpDataSourceException) { + return Result.success(); + } else { + return Result.failure(); + } + } + return Result.success(); + } + + @Override + public void onStopped() { + mCacheWriter.cancel(); + super.onStopped(); + } +} diff --git a/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java b/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java new file mode 100644 index 000000000..65f1cb995 --- /dev/null +++ b/android/src/main/java/com/jhomlala/better_player/DataSourceUtils.java @@ -0,0 +1,46 @@ +package com.jhomlala.better_player; + +import android.net.Uri; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; + +import java.util.Map; + +class DataSourceUtils { + + private static final String USER_AGENT = "User-Agent"; + private static final String USER_AGENT_PROPERTY = "http.agent"; + + static String getUserAgent(Map headers){ + String userAgent = System.getProperty(USER_AGENT_PROPERTY); + if (headers != null && headers.containsKey(USER_AGENT)) { + String userAgentHeader = headers.get(USER_AGENT); + if (userAgentHeader != null) { + userAgent = userAgentHeader; + } + } + return userAgent; + } + + static DataSource.Factory getDataSourceFactory(String userAgent, Map headers){ + DataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory() + .setUserAgent(userAgent) + .setAllowCrossProtocolRedirects(true) + .setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS) + .setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS); + + if (headers != null) { + ((DefaultHttpDataSource.Factory) dataSourceFactory).setDefaultRequestProperties(headers); + } + return dataSourceFactory; + } + + static boolean isHTTP(Uri uri) { + if (uri == null || uri.getScheme() == null) { + return false; + } + String scheme = uri.getScheme(); + return scheme.equals("http") || scheme.equals("https"); + } +} diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 1e0529fec..68656d72e 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -264,7 +264,6 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework", - "${PODS_ROOT}/../Flutter/Flutter.framework", "${BUILT_PRODUCTS_DIR}/KTVCocoaHTTPServer/KTVCocoaHTTPServer.framework", "${BUILT_PRODUCTS_DIR}/KTVHTTPCache/KTVHTTPCache.framework", "${BUILT_PRODUCTS_DIR}/better_player/better_player.framework", @@ -274,7 +273,6 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaAsyncSocket.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KTVCocoaHTTPServer.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KTVHTTPCache.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/better_player.framework", diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/lib/pages/cache_page.dart b/example/lib/pages/cache_page.dart index 417e28820..875e73b7f 100644 --- a/example/lib/pages/cache_page.dart +++ b/example/lib/pages/cache_page.dart @@ -9,6 +9,7 @@ class CachePage extends StatefulWidget { class _CachePageState extends State { late BetterPlayerController _betterPlayerController; + late BetterPlayerDataSource _betterPlayerDataSource; @override void initState() { @@ -17,13 +18,16 @@ class _CachePageState extends State { aspectRatio: 16 / 9, fit: BoxFit.contain, ); - BetterPlayerDataSource dataSource = BetterPlayerDataSource( + _betterPlayerDataSource = BetterPlayerDataSource( BetterPlayerDataSourceType.network, - Constants.forBiggerBlazesUrl, - cacheConfiguration: BetterPlayerCacheConfiguration(useCache: true), + Constants.elephantDreamVideoUrl, + cacheConfiguration: BetterPlayerCacheConfiguration( + useCache: true, + preCacheSize: 10 * 1024 * 1024, + maxCacheSize: 10 * 1024 * 1024, + maxCacheFileSize: 10 * 1024 * 1024), ); _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); - _betterPlayerController.setupDataSource(dataSource); super.initState(); } @@ -50,12 +54,30 @@ class _CachePageState extends State { aspectRatio: 16 / 9, child: BetterPlayer(controller: _betterPlayerController), ), + TextButton( + child: Text("Start pre cache"), + onPressed: () { + _betterPlayerController.preCache(_betterPlayerDataSource); + }, + ), + TextButton( + child: Text("Stop pre cache"), + onPressed: () { + _betterPlayerController.stopPreCache(_betterPlayerDataSource); + }, + ), + TextButton( + child: Text("Play video"), + onPressed: () { + _betterPlayerController.setupDataSource(_betterPlayerDataSource); + }, + ), TextButton( child: Text("Clear cache"), onPressed: () { _betterPlayerController.clearCache(); }, - ) + ), ], ), ); diff --git a/example/lib/pages/normal_player_page.dart b/example/lib/pages/normal_player_page.dart index 87e0e15bb..c753455f7 100644 --- a/example/lib/pages/normal_player_page.dart +++ b/example/lib/pages/normal_player_page.dart @@ -20,8 +20,10 @@ class _NormalPlayerPageState extends State { autoPlay: true, ); _betterPlayerController = BetterPlayerController(betterPlayerConfiguration); - _betterPlayerController.setupDataSource( - BetterPlayerDataSource.network(Constants.forBiggerBlazesUrl)); + _betterPlayerController.setupDataSource(BetterPlayerDataSource( + BetterPlayerDataSourceType.network, + "https://s038.jetfilmvid.com/videoplayback?K2ZIdkZ6NEFaV0NwUjJ0dlNiY1p2NWVlY25XVWtoVk9icFJPQzZwS2cxSEQ1aEs3dE1uYXlUVW80WUhwT2JYUjo6r%2BTqrxkaUUtrUromydVAKw%3D%3D", + )); super.initState(); } diff --git a/ios/Classes/FLTBetterPlayerPlugin.m b/ios/Classes/FLTBetterPlayerPlugin.m index 9d4deeb07..8bbdf9206 100644 --- a/ios/Classes/FLTBetterPlayerPlugin.m +++ b/ios/Classes/FLTBetterPlayerPlugin.m @@ -59,6 +59,7 @@ @interface FLTBetterPlayer : NSObject 2.0) { result([FlutterError errorWithCode:@"unsupported_speed" @@ -594,7 +598,7 @@ - (void)setSpeed:(double)speed result:(FlutterResult)result { details:nil]); } else if ((speed > 1.0 && _player.currentItem.canPlayFastForward) || (speed < 1.0 && _player.currentItem.canPlaySlowForward)) { - _player.rate = speed; + _playerRate = speed; result(nil); } else { if (speed > 1.0) { @@ -607,6 +611,10 @@ - (void)setSpeed:(double)speed result:(FlutterResult)result { details:nil]); } } + + if (_isPlaying){ + _player.rate = _playerRate; + } } diff --git a/lib/src/configuration/better_player_cache_configuration.dart b/lib/src/configuration/better_player_cache_configuration.dart index 43766b75d..cf473681e 100644 --- a/lib/src/configuration/better_player_cache_configuration.dart +++ b/lib/src/configuration/better_player_cache_configuration.dart @@ -16,9 +16,13 @@ class BetterPlayerCacheConfiguration { /// Android only option. final int maxCacheFileSize; + /// The size to download. + final int preCacheSize; + const BetterPlayerCacheConfiguration({ this.useCache = false, this.maxCacheSize = 10 * 1024 * 1024, this.maxCacheFileSize = 10 * 1024 * 1024, + this.preCacheSize = 3 * 1024 * 1024, }); } diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index 42a61a373..b838bbf35 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -445,6 +445,9 @@ class BetterPlayerController { ///run on player start. Future _initializeVideo() async { setLooping(betterPlayerConfiguration.looping); + _videoEventStreamSubscription?.cancel(); + _videoEventStreamSubscription = null; + _videoEventStreamSubscription = videoPlayerController ?.videoEventStreamController.stream .listen(_handleVideoEvent); @@ -731,6 +734,10 @@ class BetterPlayerController { _nextVideoTime = betterPlayerPlaylistConfiguration!.nextVideoDelay.inSeconds; nextVideoTimeStreamController.add(_nextVideoTime); + if (_nextVideoTime == 0) { + return; + } + _nextVideoTimer = Timer.periodic(const Duration(milliseconds: 1000), (_timer) async { if (_nextVideoTime == 1) { @@ -1044,11 +1051,8 @@ class BetterPlayerController { ///Clear all cached data. Video player controller must be initialized to ///clear the cache. - void clearCache() { - if (videoPlayerController == null) { - throw StateError("The data source has not been initialized"); - } - videoPlayerController!.clearCache(); + Future clearCache() async { + return VideoPlayerController.clearCache(); } ///Build headers map that will be used to setup video player controller. Apply @@ -1064,6 +1068,40 @@ class BetterPlayerController { return headers; } + ///PreCache a video. Currently supports Android only. The future succeed when + ///the requested size, specified in + ///[BetterPlayerCacheConfiguration.preCacheSize], is downloaded or when the + ///complete file is downloaded if the file is smaller than the requested size. + Future preCache(BetterPlayerDataSource betterPlayerDataSource) async { + if (!Platform.isAndroid) { + return Future.error("preCache is currently only supported on Android."); + } + + final cacheConfig = betterPlayerDataSource.cacheConfiguration ?? + const BetterPlayerCacheConfiguration(useCache: true); + + final dataSource = DataSource( + sourceType: DataSourceType.network, + uri: betterPlayerDataSource.url, + useCache: true, + headers: betterPlayerDataSource.headers, + maxCacheSize: cacheConfig.maxCacheSize, + maxCacheFileSize: cacheConfig.maxCacheFileSize); + + return VideoPlayerController.preCache(dataSource, cacheConfig.preCacheSize); + } + + ///Stop pre cache for given [betterPlayerDataSource]. If there was no pre + ///cache started for given [betterPlayerDataSource] then it will be ignored. + Future stopPreCache( + BetterPlayerDataSource betterPlayerDataSource) async { + if (!Platform.isAndroid) { + return Future.error( + "stopPreCache is currently only supported on Android."); + } + return VideoPlayerController?.stopPreCache(betterPlayerDataSource.url); + } + /// Add controller internal event. void _postControllerEvent(BetterPlayerControllerEvent event) { _controllerEventStreamController.add(event); diff --git a/lib/src/core/better_player_with_controls.dart b/lib/src/core/better_player_with_controls.dart index e1a9334fb..ae9d19d4b 100644 --- a/lib/src/core/better_player_with_controls.dart +++ b/lib/src/core/better_player_with_controls.dart @@ -302,16 +302,17 @@ class _BetterPlayerVideoFitWidgetState Widget build(BuildContext context) { if (_initialized && _started) { return Center( - child: Container( - width: double.infinity, - height: double.infinity, - child: FittedBox( - fit: widget.boxFit, - child: SizedBox( - width: controller!.value.size?.width ?? 0, - height: controller!.value.size?.height ?? 0, - child: VideoPlayer(controller), - // + child: ClipRect( + child: Container( + width: double.infinity, + height: double.infinity, + child: FittedBox( + fit: widget.boxFit, + child: SizedBox( + width: controller!.value.size?.width ?? 0, + height: controller!.value.size?.height ?? 0, + child: VideoPlayer(controller), + ), ), ), ), diff --git a/lib/src/video_player/method_channel_video_player.dart b/lib/src/video_player/method_channel_video_player.dart index a688e817e..89cf6be3e 100644 --- a/lib/src/video_player/method_channel_video_player.dart +++ b/lib/src/video_player/method_channel_video_player.dart @@ -104,6 +104,34 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { return; } + @override + Future preCache(DataSource dataSource, int preCacheSize) { + final Map dataSourceDescription = { + 'key': dataSource.key, + 'uri': dataSource.uri, + 'headers': dataSource.headers, + 'maxCacheSize': dataSource.maxCacheSize, + 'maxCacheFileSize': dataSource.maxCacheFileSize, + 'preCacheSize': preCacheSize + }; + return _channel.invokeMethod( + 'preCache', + { + 'dataSource': dataSourceDescription, + }, + ); + } + + @override + Future stopPreCache(String url) { + return _channel.invokeMethod( + 'stopPreCache', + { + 'url': url, + }, + ); + } + @override Future setLooping(int? textureId, bool looping) { return _channel.invokeMethod( @@ -260,12 +288,10 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { } @override - Future clearCache(int? textureId) { + Future clearCache() { return _channel.invokeMethod( 'clearCache', - { - 'textureId': textureId, - }, + {}, ); } diff --git a/lib/src/video_player/video_player.dart b/lib/src/video_player/video_player.dart index eea063433..ee7f884ce 100644 --- a/lib/src/video_player/video_player.dart +++ b/lib/src/video_player/video_player.dart @@ -579,8 +579,16 @@ class VideoPlayerController extends ValueNotifier { _videoPlayerPlatform.setMixWithOthers(_textureId, mixWithOthers); } - void clearCache() { - _videoPlayerPlatform.clearCache(_textureId); + static Future clearCache() async { + return _videoPlayerPlatform.clearCache(); + } + + static Future preCache(DataSource dataSource, int preCacheSize) async { + return _videoPlayerPlatform.preCache(dataSource, preCacheSize); + } + + static Future stopPreCache(String url) async { + return _videoPlayerPlatform.stopPreCache(url); } } diff --git a/lib/src/video_player/video_player_platform_interface.dart b/lib/src/video_player/video_player_platform_interface.dart index 44f1b8b4a..a1ba634a7 100644 --- a/lib/src/video_player/video_player_platform_interface.dart +++ b/lib/src/video_player/video_player_platform_interface.dart @@ -74,6 +74,16 @@ abstract class VideoPlayerPlatform { throw UnimplementedError('create() has not been implemented.'); } + /// Pre-caches a video. + Future preCache(DataSource dataSource, int preCacheSize) { + throw UnimplementedError('preCache() has not been implemented.'); + } + + /// Pre-caches a video. + Future stopPreCache(String url) { + throw UnimplementedError('stopPreCache() has not been implemented.'); + } + /// Set data source of video. Future setDataSource(int? textureId, DataSource dataSource) { throw UnimplementedError('setDataSource() has not been implemented.'); @@ -156,7 +166,7 @@ abstract class VideoPlayerPlatform { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } - Future clearCache(int? textureId) { + Future clearCache() { throw UnimplementedError('clearCache() has not been implemented.'); } diff --git a/pubspec.lock b/pubspec.lock index 09f6d8bef..0b1700819 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -291,7 +291,7 @@ packages: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.5.0+2" wakelock_macos: dependency: transitive description: @@ -313,6 +313,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + wakelock_windows: + dependency: transitive + description: + name: wakelock_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6c4be029a..f657c1a69 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.63 +version: 0.0.64 authors: - Jakub Homlala homepage: https://github.com/jhomlala/betterplayer @@ -13,7 +13,7 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 - wakelock: ^0.4.0 + wakelock: ^0.5.0+2 pedantic: ^1.11.0 meta: ^1.3.0 flutter_widget_from_html_core: ^0.6.0-rc.2021030201