feat(android,interface): Implement video thumbnail generation

This commit is contained in:
PapaTutuWawa 2023-08-24 20:06:31 +02:00
parent fb71ac330a
commit fe6d0a60c1
12 changed files with 183 additions and 2 deletions

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:path/path.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
/// The id of the notification channel. /// The id of the notification channel.
@ -286,6 +287,43 @@ class MyHomePageState extends State<MyHomePage> {
}, },
child: const Text('Open battery optimisation page'), 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'),
)
], ],
), ),
), ),

View File

@ -39,6 +39,8 @@ dependencies:
file_picker: 5.2.0+1 file_picker: 5.2.0+1
path: 1.8.3
permission_handler: 10.4.3 permission_handler: 10.4.3
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.

View File

@ -10,7 +10,7 @@
<application> <application>
<provider <provider
android:name="me.polynom.moxplatform_android.MoxplatformFileProvider" 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:exported="false"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data

View File

@ -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 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); 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 */ /** Stubs */
void eventStub(@NonNull NotificationEvent event); void eventStub(@NonNull NotificationEvent event);
@ -1466,6 +1469,32 @@ public class Api {
channel.setMessageHandler(null); 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 = BasicMessageChannel<Object> channel =
new BasicMessageChannel<>( new BasicMessageChannel<>(

View File

@ -25,7 +25,7 @@ const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime" const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path" 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 // Shared preferences keys
const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android" const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"

View File

@ -3,9 +3,11 @@ package me.polynom.moxplatform_android;
import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
import static androidx.core.content.ContextCompat.getSystemService; import static androidx.core.content.ContextCompat.getSystemService;
import static androidx.core.content.ContextCompat.startActivity; 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.ConstantsKt.SHARED_PREFERENCES_KEY;
import static me.polynom.moxplatform_android.CryptoKt.*; import static me.polynom.moxplatform_android.CryptoKt.*;
import static me.polynom.moxplatform_android.RecordSentMessageKt.*; import static me.polynom.moxplatform_android.RecordSentMessageKt.*;
import static me.polynom.moxplatform_android.ThumbnailsKt.generateVideoThumbnailImplementation;
import me.polynom.moxplatform_android.Api.*; import me.polynom.moxplatform_android.Api.*;
@ -13,20 +15,31 @@ import android.app.ActivityManager;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.PowerManager; import android.os.PowerManager;
import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -313,6 +326,12 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt
CryptoKt.hashFile(sourcePath, hashSpec, result); 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 @Override
public void eventStub(@NonNull NotificationEvent event) { public void eventStub(@NonNull NotificationEvent event) {
// Stub to trick pigeon into // Stub to trick pigeon into

View File

@ -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;
}
}

View File

@ -1,3 +1,4 @@
import 'package:moxplatform/moxplatform.dart';
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
class AndroidPlatformImplementation extends PlatformImplementation { class AndroidPlatformImplementation extends PlatformImplementation {
@ -20,4 +21,10 @@ class AndroidPlatformImplementation extends PlatformImplementation {
Future<void> openBatteryOptimisationSettings() { Future<void> openBatteryOptimisationSettings() {
return MoxplatformInterface.api.openBatteryOptimisationSettings(); return MoxplatformInterface.api.openBatteryOptimisationSettings();
} }
@override
Future<bool> generateVideoThumbnail(
String src, String dest, int width) async {
return MoxplatformInterface.api.generateVideoThumbnail(src, dest, width);
}
} }

View File

@ -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 /// Stubs
Future<void> eventStub(NotificationEvent arg_event) async { Future<void> eventStub(NotificationEvent arg_event) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(

View File

@ -12,4 +12,9 @@ abstract class PlatformImplementation {
/// Opens the page for battery optimisations. If not supported on the /// Opens the page for battery optimisations. If not supported on the
/// platform, does nothing. /// platform, does nothing.
Future<void> openBatteryOptimisationSettings(); 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);
} }

View File

@ -14,4 +14,9 @@ class StubPlatformImplementation extends PlatformImplementation {
@override @override
Future<void> openBatteryOptimisationSettings() async {} Future<void> openBatteryOptimisationSettings() async {}
@override
Future<bool> generateVideoThumbnail(
String src, String dest, int width) async =>
false;
} }

View File

@ -192,6 +192,9 @@ abstract class MoxplatformApi {
@async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec); @async CryptographyResult? decryptFile(String sourcePath, String destPath, Uint8List key, Uint8List iv, CipherAlgorithm algorithm, String hashSpec);
@async Uint8List? hashFile(String sourcePath, String hashSpec); @async Uint8List? hashFile(String sourcePath, String hashSpec);
/// Media APIs
bool generateVideoThumbnail(String src, String dest, int maxWidth);
/// Stubs /// Stubs
void eventStub(NotificationEvent event); void eventStub(NotificationEvent event);
} }