load video embedded thumbnail
This commit is contained in:
parent
8f94af28d3
commit
8c265b6479
15 changed files with 353 additions and 211 deletions
|
@ -3,7 +3,8 @@ package deckers.thibault.aves.channelhandlers;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
|
@ -6,7 +6,8 @@ import android.app.AlertDialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.karumi.dexter.Dexter;
|
import com.karumi.dexter.Dexter;
|
||||||
import com.karumi.dexter.PermissionToken;
|
import com.karumi.dexter.PermissionToken;
|
||||||
|
|
|
@ -8,12 +8,15 @@ import android.util.Log;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.Key;
|
import com.bumptech.glide.load.Key;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
import com.bumptech.glide.signature.ObjectKey;
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import deckers.thibault.aves.decoder.VideoThumbnail;
|
||||||
import deckers.thibault.aves.model.ImageEntry;
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
import deckers.thibault.aves.utils.Utils;
|
import deckers.thibault.aves.utils.Utils;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
@ -61,11 +64,28 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
|
||||||
if (!this.isCancelled()) {
|
if (!this.isCancelled()) {
|
||||||
// add signature to ignore cache for images which got modified but kept the same URI
|
// add signature to ignore cache for images which got modified but kept the same URI
|
||||||
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
Key signature = new ObjectKey("" + entry.getDateModifiedSecs() + entry.getWidth() + entry.getOrientationDegrees());
|
||||||
FutureTarget<Bitmap> target = Glide.with(activity)
|
RequestOptions options = new RequestOptions()
|
||||||
|
.signature(signature)
|
||||||
|
.override(p.width, p.height);
|
||||||
|
|
||||||
|
FutureTarget<Bitmap> target;
|
||||||
|
if (entry.isVideo()) {
|
||||||
|
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE);
|
||||||
|
target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
|
.load(new VideoThumbnail(activity, entry.getUri()))
|
||||||
|
.signature(signature)
|
||||||
|
.submit(p.width, p.height);
|
||||||
|
} else {
|
||||||
|
target = Glide.with(activity)
|
||||||
|
.asBitmap()
|
||||||
|
.apply(options)
|
||||||
.load(entry.getUri())
|
.load(entry.getUri())
|
||||||
.signature(signature)
|
.signature(signature)
|
||||||
.submit(p.width, p.height);
|
.submit(p.width, p.height);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Bitmap bmp = target.get();
|
Bitmap bmp = target.get();
|
||||||
if (bmp != null) {
|
if (bmp != null) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package deckers.thibault.aves.channelhandlers;
|
package deckers.thibault.aves.channelhandlers;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.adobe.xmp.XMPException;
|
import com.adobe.xmp.XMPException;
|
||||||
import com.adobe.xmp.XMPIterator;
|
import com.adobe.xmp.XMPIterator;
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package deckers.thibault.aves.decoder;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
public class VideoThumbnail {
|
||||||
|
private Context mContext;
|
||||||
|
private Uri mUri;
|
||||||
|
|
||||||
|
public VideoThumbnail(Context context, Uri uri) {
|
||||||
|
mContext = context;
|
||||||
|
mUri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri getUri() {
|
||||||
|
return mUri;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package deckers.thibault.aves.decoder;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.media.MediaMetadataRetriever;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Priority;
|
||||||
|
import com.bumptech.glide.load.DataSource;
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
class VideoThumbnailFetcher implements DataFetcher<InputStream> {
|
||||||
|
private final VideoThumbnail model;
|
||||||
|
|
||||||
|
VideoThumbnailFetcher(VideoThumbnail model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||||
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||||
|
try {
|
||||||
|
retriever.setDataSource(model.getContext(), model.getUri());
|
||||||
|
byte[] picture = retriever.getEmbeddedPicture();
|
||||||
|
if (picture != null) {
|
||||||
|
callback.onDataReady(new ByteArrayInputStream(picture));
|
||||||
|
} else {
|
||||||
|
// not ideal: bitmap -> byte[] -> bitmap
|
||||||
|
// but simple fallback and we cache result
|
||||||
|
Bitmap bitmap = retriever.getFrameAtTime();
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos);
|
||||||
|
callback.onDataReady(new ByteArrayInputStream(bos.toByteArray()));
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
callback.onLoadFailed(ex);
|
||||||
|
} finally {
|
||||||
|
retriever.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
// already cleaned up in loadData and ByteArrayInputStream will be GC'd
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
// cannot cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<InputStream> getDataClass() {
|
||||||
|
return InputStream.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DataSource getDataSource() {
|
||||||
|
return DataSource.LOCAL;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package deckers.thibault.aves.decoder;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.GlideBuilder;
|
||||||
|
import com.bumptech.glide.Registry;
|
||||||
|
import com.bumptech.glide.annotation.GlideModule;
|
||||||
|
import com.bumptech.glide.load.DecodeFormat;
|
||||||
|
import com.bumptech.glide.module.AppGlideModule;
|
||||||
|
import com.bumptech.glide.request.RequestOptions;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
public class VideoThumbnailGlideModule extends AppGlideModule {
|
||||||
|
@Override
|
||||||
|
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
|
||||||
|
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||||
|
registry.append(VideoThumbnail.class, InputStream.class, new VideoThumbnailLoader.Factory());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isManifestParsingEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package deckers.thibault.aves.decoder;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.Options;
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader;
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||||
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
class VideoThumbnailLoader implements ModelLoader<VideoThumbnail, InputStream> {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public LoadData<InputStream> buildLoadData(@NonNull VideoThumbnail model, int width, int height, @NonNull Options options) {
|
||||||
|
return new LoadData<>(new ObjectKey(model.getUri()), new VideoThumbnailFetcher(model));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handles(@NonNull VideoThumbnail videoThumbnail) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Factory implements ModelLoaderFactory<VideoThumbnail, InputStream> {
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ModelLoader<VideoThumbnail, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||||
|
return new VideoThumbnailLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void teardown() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package deckers.thibault.aves.model;
|
package deckers.thibault.aves.model;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.text.format.DateUtils;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -21,14 +21,11 @@ public class ImageEntry {
|
||||||
private String title, bucketDisplayName;
|
private String title, bucketDisplayName;
|
||||||
private long dateModifiedSecs, sourceDateTakenMillis;
|
private long dateModifiedSecs, sourceDateTakenMillis;
|
||||||
private long durationMillis;
|
private long durationMillis;
|
||||||
// derived
|
|
||||||
private boolean isVideo;
|
|
||||||
|
|
||||||
public ImageEntry(Uri uri, String mimeType) {
|
public ImageEntry(Uri uri, String mimeType) {
|
||||||
this.contentId = -1;
|
this.contentId = -1;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.mimeType = mimeType;
|
this.mimeType = mimeType;
|
||||||
init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// uri: content provider uri
|
// uri: content provider uri
|
||||||
|
@ -48,7 +45,6 @@ public class ImageEntry {
|
||||||
this.sourceDateTakenMillis = dateTakenMillis;
|
this.sourceDateTakenMillis = dateTakenMillis;
|
||||||
this.bucketDisplayName = bucketDisplayName;
|
this.bucketDisplayName = bucketDisplayName;
|
||||||
this.durationMillis = durationMillis;
|
this.durationMillis = durationMillis;
|
||||||
init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageEntry(Map map) {
|
public ImageEntry(Map map) {
|
||||||
|
@ -87,10 +83,6 @@ public class ImageEntry {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
|
||||||
isVideo = mimeType.startsWith(Constants.MIME_VIDEO);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
@ -105,7 +97,7 @@ public class ImageEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVideo() {
|
public boolean isVideo() {
|
||||||
return isVideo;
|
return mimeType.startsWith(Constants.MIME_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEditable() {
|
public boolean isEditable() {
|
||||||
|
@ -132,52 +124,10 @@ public class ImageEntry {
|
||||||
return orientationDegrees;
|
return orientationDegrees;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getSizeBytes() {
|
|
||||||
return sizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDateModifiedSecs() {
|
public long getDateModifiedSecs() {
|
||||||
return dateModifiedSecs;
|
return dateModifiedSecs;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getBucketDisplayName() {
|
|
||||||
return bucketDisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDuration() {
|
|
||||||
return DateUtils.formatElapsedTime(durationMillis / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMegaPixels() {
|
|
||||||
return Math.round((width * height) / 1000000f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// setters
|
|
||||||
|
|
||||||
public void setContentId(long contentId) {
|
|
||||||
this.contentId = contentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPath(String path) {
|
|
||||||
this.path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSizeBytes(long sizeBytes) {
|
|
||||||
this.sizeBytes = sizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDateModifiedSecs(long dateModifiedSecs) {
|
|
||||||
this.dateModifiedSecs = dateModifiedSecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// convenience method
|
// convenience method
|
||||||
|
|
||||||
private static long toLong(Object o) {
|
private static long toLong(Object o) {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.provider.MediaStore;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import deckers.thibault.aves.model.ImageEntry;
|
import deckers.thibault.aves.model.ImageEntry;
|
||||||
|
|
|
@ -32,6 +32,7 @@ import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -162,7 +163,9 @@ public class FileUtils {
|
||||||
return success ? file.getPath() : null;
|
return success ? file.getPath() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return extension of image with dot, or default .jpg if it none. */
|
/**
|
||||||
|
* @return extension of image with dot, or default .jpg if it none.
|
||||||
|
*/
|
||||||
private static String getImageExtension(Context context, Uri uriImage) {
|
private static String getImageExtension(Context context, Uri uriImage) {
|
||||||
String extension = null;
|
String extension = null;
|
||||||
|
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ImageDecodeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getImageBytes(ImageEntry entry, int width, int height) async {
|
static Future<Uint8List> getImageBytes(ImageEntry entry, int width, int height) async {
|
||||||
debugPrint('getImageBytes with uri=${entry.uri}');
|
debugPrint('getImageBytes with path=${entry.path} contentId=${entry.contentId}');
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
try {
|
try {
|
||||||
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
final result = await platform.invokeMethod('getImageBytes', <String, dynamic>{
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'mime_types.dart';
|
||||||
|
|
||||||
class ImageEntry {
|
class ImageEntry {
|
||||||
String uri;
|
String uri;
|
||||||
String path;
|
String path;
|
||||||
|
@ -65,6 +67,8 @@ class ImageEntry {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isVideo => mimeType.startsWith(MimeTypes.MIME_VIDEO);
|
||||||
|
|
||||||
int getMegaPixels() {
|
int getMegaPixels() {
|
||||||
return ((width * height) / 1000000).round();
|
return ((width * height) / 1000000).round();
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ class InfoPageState extends State<InfoPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final date = entry.getBestDate();
|
final date = entry.getBestDate();
|
||||||
|
final dateText = '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}';
|
||||||
|
final resolutionText = '${entry.width} × ${entry.height}${entry.isVideo ? '': ' (${entry.getMegaPixels()} MP)'}';
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
|
@ -72,8 +74,8 @@ class InfoPageState extends State<InfoPage> {
|
||||||
children: [
|
children: [
|
||||||
SectionRow('File'),
|
SectionRow('File'),
|
||||||
InfoRow('Title', entry.title),
|
InfoRow('Title', entry.title),
|
||||||
InfoRow('Date', '${DateFormat.yMMMd().format(date)} – ${DateFormat.Hm().format(date)}'),
|
InfoRow('Date', dateText),
|
||||||
InfoRow('Resolution', '${entry.width} × ${entry.height} (${entry.getMegaPixels()} MP)'),
|
InfoRow('Resolution', resolutionText),
|
||||||
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
InfoRow('Size', formatFilesize(entry.sizeBytes)),
|
||||||
InfoRow('Path', entry.path),
|
InfoRow('Path', entry.path),
|
||||||
SectionRow('Metadata'),
|
SectionRow('Metadata'),
|
||||||
|
|
Loading…
Reference in a new issue