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: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'),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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<>(
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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';
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?>(
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user