Skip to content

Commit

Permalink
Feature/april changes (#426)
Browse files Browse the repository at this point in the history
* Updated changelog

* Video fit fixes

* Fixed set speed iOS implementation

* Fixed set speed iOS issue

* Fixed Android's notification image OOM issue.

* Fixed Android's notification image OOM issue.

* Fixed 0 second delay issue in playlist.

* Fixed 0 second delay issue in playlist.

* 0.0.64 version update, updated changelog

* Added pre-caching feature on Android (#397)

* Added pre-caching feature on Android

* Improved pre-cache error handling

* Fixes video painted out of bounds bug

* Refactored to remove duplicates

Co-authored-by: Jakub <[email protected]>

* Pre cache refactor

* Pre cache refactor

* General refactor

* General refactor

* General refactor

Co-authored-by: themadmrj <[email protected]>
  • Loading branch information
jhomlala and themadmrj authored Apr 11, 2021
1 parent e0f0b54 commit 679a749
Show file tree
Hide file tree
Showing 21 changed files with 489 additions and 79 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

Expand Down
122 changes: 88 additions & 34 deletions android/src/main/java/com/jhomlala/better_player/BetterPlayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand All @@ -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<BetterPlayer> videoPlayers = new LongSparseArray<>();
private final LongSparseArray<Map<String, Object>> dataSources = new LongSparseArray<>();
Expand All @@ -102,6 +110,7 @@ public class BetterPlayerPlugin implements FlutterPlugin, ActivityAware, MethodC
public BetterPlayerPlugin() {
}

@SuppressWarnings("deprecation")
private BetterPlayerPlugin(Registrar registrar) {
this.flutterState =
new FlutterState(
Expand All @@ -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);

Expand All @@ -127,6 +137,7 @@ public static void registerWith(Registrar registrar) {

}

@SuppressWarnings("deprecation")
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
this.flutterState =
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String, Object> 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<String, String> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 679a749

Please sign in to comment.