feat(android,interface): Implement video thumbnail generation
This commit is contained in:
parent
fb71ac330a
commit
fe6d0a60c1
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
/// The id of the notification channel.
|
||||
@ -286,6 +287,43 @@ class MyHomePageState extends State<MyHomePage> {
|
||||
},
|
||||
child: const Text('Open battery optimisation page'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.video,
|
||||
);
|
||||
if (result == null) return;
|
||||
|
||||
final path = result.files.single.path!;
|
||||
final storagePath =
|
||||
await MoxplatformPlugin.platform.getPersistentDataPath();
|
||||
final mediaPath = join(storagePath, 'media');
|
||||
if (!Directory(mediaPath).existsSync()) {
|
||||
await Directory(mediaPath).create(recursive: true);
|
||||
}
|
||||
|
||||
final internalPath = join(mediaPath, basename(path));
|
||||
print('Copying file');
|
||||
await File(path).copy(internalPath);
|
||||
|
||||
print('Generating thumbnail');
|
||||
final thumbResult =
|
||||
await MoxplatformPlugin.platform.generateVideoThumbnail(
|
||||
internalPath,
|
||||
'$internalPath.thumbnail.jpg',
|
||||
720,
|
||||
);
|
||||
print('Success: $thumbResult');
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => Image.file(
|
||||
File('$internalPath.thumbnail.jpg'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Thumbnail'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -39,6 +39,8 @@ dependencies:
|
||||
|
||||
file_picker: 5.2.0+1
|
||||
|
||||
path: 1.8.3
|
||||
|
||||
permission_handler: 10.4.3
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
@ -10,7 +10,7 @@
|
||||
<application>
|
||||
<provider
|
||||
android:name="me.polynom.moxplatform_android.MoxplatformFileProvider"
|
||||
android:authorities="me.polynom.moxplatform_android.fileprovider"
|
||||
android:authorities="me.polynom.moxplatform_android.fileprovider2"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
|
@ -1097,6 +1097,9 @@ public class Api {
|
||||
void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result<CryptographyResult> result);
|
||||
|
||||
void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Result<byte[]> result);
|
||||
/** Media APIs */
|
||||
@NonNull
|
||||
Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth);
|
||||
/** Stubs */
|
||||
void eventStub(@NonNull NotificationEvent event);
|
||||
|
||||
@ -1466,6 +1469,32 @@ public class Api {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<Object>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
String srcArg = (String) args.get(0);
|
||||
String destArg = (String) args.get(1);
|
||||
Number maxWidthArg = (Number) args.get(2);
|
||||
try {
|
||||
Boolean output = api.generateVideoThumbnail(srcArg, destArg, (maxWidthArg == null) ? null : maxWidthArg.longValue());
|
||||
wrapped.add(0, output);
|
||||
}
|
||||
catch (Throwable exception) {
|
||||
ArrayList<Object> wrappedError = wrapError(exception);
|
||||
wrapped = wrappedError;
|
||||
}
|
||||
reply.reply(wrapped);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
|
@ -25,7 +25,7 @@ const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
|
||||
const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
|
||||
const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path"
|
||||
|
||||
const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider"
|
||||
const val MOXPLATFORM_FILEPROVIDER_ID = "me.polynom.moxplatform_android.fileprovider2"
|
||||
|
||||
// Shared preferences keys
|
||||
const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"
|
||||
|
@ -3,9 +3,11 @@ package me.polynom.moxplatform_android;
|
||||
import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
|
||||
import static androidx.core.content.ContextCompat.getSystemService;
|
||||
import static androidx.core.content.ContextCompat.startActivity;
|
||||
import static me.polynom.moxplatform_android.ConstantsKt.MOXPLATFORM_FILEPROVIDER_ID;
|
||||
import static me.polynom.moxplatform_android.ConstantsKt.SHARED_PREFERENCES_KEY;
|
||||
import static me.polynom.moxplatform_android.CryptoKt.*;
|
||||
import static me.polynom.moxplatform_android.RecordSentMessageKt.*;
|
||||
import static me.polynom.moxplatform_android.ThumbnailsKt.generateVideoThumbnailImplementation;
|
||||
|
||||
import me.polynom.moxplatform_android.Api.*;
|
||||
|
||||
@ -13,20 +15,31 @@ import android.app.ActivityManager;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.media.ThumbnailUtils;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -313,6 +326,12 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
|
||||
CryptoKt.hashFile(sourcePath, hashSpec, result);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Boolean generateVideoThumbnail(@NonNull String src, @NonNull String dest, @NonNull Long maxWidth) {
|
||||
return generateVideoThumbnailImplementation(src, dest, maxWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventStub(@NonNull NotificationEvent event) {
|
||||
// Stub to trick pigeon into
|
||||
|
@ -0,0 +1,43 @@
|
||||
package me.polynom.moxplatform_android
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.util.Log
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/*
|
||||
* Generate a video thumbnail using the first frame of the video at @src. Afterwards, scale it
|
||||
* down such that its width is equal to @maxWidth (while keeping the aspect ratio) and write it to
|
||||
* @dest.
|
||||
*
|
||||
* If everything went well, returns true. If we're unable to generate the thumbnail, returns false.
|
||||
* */
|
||||
fun generateVideoThumbnailImplementation(src: String, dest: String, maxWidth: Long): Boolean {
|
||||
try {
|
||||
val mmr = MediaMetadataRetriever().apply {
|
||||
setDataSource(src)
|
||||
}
|
||||
val unscaledThumbnail = mmr.getFrameAtTime(0) ?: return false
|
||||
|
||||
// Scale down the thumbnail while keeping the aspect ratio
|
||||
val scalingFactor = maxWidth.toDouble() / unscaledThumbnail.width;
|
||||
Log.d(TAG, "Scaling to $maxWidth from ${unscaledThumbnail.width} with scalingFactor $scalingFactor");
|
||||
val thumbnail = Bitmap.createScaledBitmap(
|
||||
unscaledThumbnail,
|
||||
(unscaledThumbnail.width * scalingFactor).toInt(),
|
||||
(unscaledThumbnail.height * scalingFactor).toInt(),
|
||||
false,
|
||||
)
|
||||
|
||||
// Write it to the destination file
|
||||
val fos = FileOutputStream(dest)
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
return true;
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Failed to create thumbnail for $src: ${ex.message}")
|
||||
ex.printStackTrace()
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
|
||||
|
||||
class AndroidPlatformImplementation extends PlatformImplementation {
|
||||
@ -20,4 +21,10 @@ class AndroidPlatformImplementation extends PlatformImplementation {
|
||||
Future<void> openBatteryOptimisationSettings() {
|
||||
return MoxplatformInterface.api.openBatteryOptimisationSettings();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> generateVideoThumbnail(
|
||||
String src, String dest, int width) async {
|
||||
return MoxplatformInterface.api.generateVideoThumbnail(src, dest, width);
|
||||
}
|
||||
}
|
||||
|
@ -756,6 +756,36 @@ class MoxplatformApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Media APIs
|
||||
Future<bool> generateVideoThumbnail(
|
||||
String arg_src, String arg_dest, int arg_maxWidth) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.generateVideoThumbnail',
|
||||
codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_src, arg_dest, arg_maxWidth]) as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel.',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else if (replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (replyList[0] as bool?)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stubs
|
||||
Future<void> eventStub(NotificationEvent arg_event) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
|
@ -12,4 +12,9 @@ abstract class PlatformImplementation {
|
||||
/// Opens the page for battery optimisations. If not supported on the
|
||||
/// platform, does nothing.
|
||||
Future<void> openBatteryOptimisationSettings();
|
||||
|
||||
/// Attempt to generate a thumbnail for the video file at [src], scale it, while keeping the
|
||||
/// aspect ratio in tact to [width], and write it to [dest]. If we were successful, returns true.
|
||||
/// If no thumbnail was generated, returns false.
|
||||
Future<bool> generateVideoThumbnail(String src, String dest, int width);
|
||||
}
|
||||
|
@ -14,4 +14,9 @@ class StubPlatformImplementation extends PlatformImplementation {
|
||||
|
||||
@override
|
||||
Future<void> openBatteryOptimisationSettings() async {}
|
||||
|
||||
@override
|
||||
Future<bool> generateVideoThumbnail(
|
||||
String src, String dest, int width) async =>
|
||||
false;
|
||||
}
|
||||
|
@ -192,6 +192,9 @@ abstract class MoxplatformApi {
|
||||
@async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec);
|
||||
@async Uint8List? hashFile(String sourcePath, String hashSpec);
|
||||
|
||||
/// Media APIs
|
||||
bool generateVideoThumbnail(String src, String dest, int maxWidth);
|
||||
|
||||
/// Stubs
|
||||
void eventStub(NotificationEvent event);
|
||||
}
|
||||
|
Reference in New Issue
Block a user