fc_native_video_thumbnail 2.1.1
fc_native_video_thumbnail: ^2.1.1 copied to clipboard
A Flutter plugin to create video thumbnails via native APIs.
import 'dart:async';
import 'dart:io';
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
import 'package:saf_util/saf_util.dart';
import 'package:tmp_path/tmp_path.dart';
void main() {
runApp(const MyApp());
}
final _plugin = FcNativeVideoThumbnail();
final _safUtil = SafUtil();
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MyHome());
}
}
class Task {
final String name;
final String srcFile;
final int width;
final int height;
final bool? isSrcUri;
String? outFile;
String? outFileDimensions;
String? outFileError;
Uint8List? outBytes;
String? outBytesDimensions;
String? outBytesError;
Task({
required this.name,
required this.srcFile,
required this.width,
required this.height,
this.isSrcUri,
});
Future<void> run() async {
await Future.wait([_toFile(), _toBytes()]);
}
Future<void> _toFile() async {
try {
final destFile = tmpPath() + p.extension(srcFile);
await _plugin.saveThumbnailToFile(
srcFile: srcFile,
destFile: destFile,
width: width,
height: height,
srcFileUri: isSrcUri,
format: 'jpeg',
);
if (await File(destFile).exists()) {
var imageFile = File(destFile);
var decodedImage = await decodeImageFromList(
await imageFile.readAsBytes(),
);
outFileDimensions =
'Decoded size: ${decodedImage.width}x${decodedImage.height}';
outFile = destFile;
} else {
outFileError = 'No thumbnail extracted';
}
} catch (err) {
outFileError = err.toString();
}
}
Future<void> _toBytes() async {
try {
final bytes = await _plugin.saveThumbnailToBytes(
srcFile: srcFile,
width: width,
height: height,
srcFileUri: isSrcUri,
format: 'jpeg',
);
if (bytes != null) {
var decodedImage = await decodeImageFromList(bytes);
outBytesDimensions =
'Decoded size: ${decodedImage.width}x${decodedImage.height}';
outBytes = bytes;
} else {
outBytesError = 'No thumbnail extracted';
}
} catch (err) {
outBytesError = err.toString();
}
}
}
class MyHome extends StatefulWidget {
const MyHome({super.key});
@override
State<MyHome> createState() => _MyHomeState();
}
enum _VideoSrc { gallery, files, uri }
class _MyHomeState extends State<MyHome> {
final _tasks = <Task>[];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Plugin example app')),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
if (Platform.isAndroid || Platform.isIOS) ...[
ElevatedButton(
onPressed: () => _selectVideo(.gallery),
child: const Text('Select video from gallery'),
),
],
ElevatedButton(
onPressed: () => _selectVideo(.files),
child: const Text('Select video from files'),
),
if (Platform.isAndroid)
ElevatedButton(
onPressed: () => _selectVideo(.uri),
child: const Text('Select video from SAF (URI)'),
),
..._tasks.map((task) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Text(
'>>> ${task.name}',
style: const TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
),
Text('Out file: ${task.outFile}'),
if (task.outFileError != null) ...[
Text(
task.outFileError!,
style: const TextStyle(color: Colors.red),
),
],
if (task.outFile != null) ...[
Text(task.outFileDimensions ?? ''),
Image(image: FileImage(File(task.outFile!))),
],
Text(
'Out bytes: ${task.outBytes != null ? '${task.outBytes!.lengthInBytes} bytes' : 'null'}',
),
if (task.outBytesError != null) ...[
Text(
task.outBytesError!,
style: const TextStyle(color: Colors.red),
),
],
if (task.outBytes != null) ...[
Text(task.outBytesDimensions ?? ''),
Image.memory(task.outBytes!),
],
],
);
}),
],
),
),
),
);
}
Future<void> _selectVideo(_VideoSrc src) async {
try {
String? srcPath;
if (src == _VideoSrc.uri) {
if (Platform.isAndroid) {
final doc = await _safUtil.pickFile();
if (doc == null) {
return;
}
srcPath = doc.uri;
} else {
throw Exception('Should not reach here');
}
} else if (src == _VideoSrc.files) {
var src = await openFile();
if (src == null) {
return;
}
srcPath = src.path;
} else {
final picker = ImagePicker();
final xfile = await picker.pickVideo(source: ImageSource.gallery);
if (xfile == null) {
return;
}
srcPath = xfile.path;
}
setState(() {
_tasks.clear();
});
final smallVidBytes = await rootBundle.load('res/a.mp4');
final smallVidPath = '${tmpPath()}_small.mp4';
await File(smallVidPath).writeAsBytes(smallVidBytes.buffer.asUint8List());
_tasks.add(
Task(
name: 'Resize to 300x300',
srcFile: srcPath,
isSrcUri: src == _VideoSrc.uri ? true : null,
width: 300,
height: 300,
),
);
// Upscaling task.
_tasks.add(
Task(
name: 'No upscaling to 1000x1000',
srcFile: smallVidPath,
width: 1000,
height: 1000,
),
);
await Future.forEach(_tasks, (Task task) async {
await task.run();
setState(() {});
});
} catch (err) {
if (!mounted) {
return;
}
await _showErrorAlert(context, err.toString());
}
}
Future<void> _showErrorAlert(BuildContext context, String msg) async {
return showDialog<void>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const SelectableText('Error'),
content: SelectableText(msg),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
}