video_web_compressor 0.0.1
video_web_compressor: ^0.0.1 copied to clipboard
A Flutter web package for video compression and format conversion using FFmpeg WebAssembly
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:video_compressor_example/widgets/video_action_button.dart';
import 'package:video_web_compressor/video_web_compressor.dart';
import 'controllers/video_processing_controller.dart';
import 'widgets/compression_recommendation_dialog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Video Processor Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const VideoProcessorPage(),
debugShowCheckedModeBanner: false,
);
}
}
class VideoProcessorPage extends StatefulWidget {
const VideoProcessorPage({super.key});
@override
State<VideoProcessorPage> createState() => _VideoProcessorPageState();
}
class _VideoProcessorPageState extends State<VideoProcessorPage> {
late final VideoProcessingController _controller;
Uint8List? _currentVideoBytes;
@override
void initState() {
super.initState();
final compressionConfig = VideoCompressionConfig.hd720p(
fps: 24,
crf: 28,
);
_controller = VideoProcessingController(
compressionConfig: compressionConfig,
/// Optional: Provide upload callback for developers to implement
onUpload: _handleUpload,
);
_controller.progressStream.listen((progress) {
debugPrint('Progress update: ${(progress * 100).toStringAsFixed(1)}%');
_controller.updateProgress(progress);
});
_controller.logStream.listen((log) {
debugPrint('FFmpeg log: $log');
});
_initializeProcessor();
}
/// Example upload implementation (developers customize this)
Future<UploadResult> _handleUpload({
required Uint8List videoBytes,
required VideoMetadata metadata,
required String filename,
}) async {
try {
debugPrint('Uploading: $filename (${videoBytes.length} bytes)');
/// TODO: Implement your upload logic here
/// Example using HTTP:
///
/// final request = http.MultipartRequest('POST', Uri.parse('your-api-url'));
/// request.files.add(http.MultipartFile.fromBytes(
/// 'video',
/// videoBytes,
/// filename: filename,
/// ));
/// final response = await request.send();
///
/// if (response.statusCode == 200) {
/// return UploadResult.success(url: 'https://your-url.com/video');
/// }
/// Simulate upload delay
await Future.delayed(const Duration(seconds: 2));
/// Return mock success
return UploadResult.success(
url: 'https://example.com/videos/$filename',
data: {'size': videoBytes.length, 'duration': metadata.duration},
);
} catch (e) {
return UploadResult.failure('Upload failed: $e');
}
}
Future<void> _initializeProcessor() async {
try {
await _controller.initialize();
} catch (e) {
if (mounted) {
_showMessage('Initialization failed: $e');
}
}
}
Future<void> _pickAndAnalyzeVideo() async {
if (!_controller.isInitialized) {
_showMessage('Processor not initialized yet');
return;
}
debugPrint('Opening file picker...');
final result = await FilePicker.platform.pickFiles(
type: FileType.video,
withData: true,
);
if (result == null || result.files.first.bytes == null) {
debugPrint('File picker cancelled or no file selected');
return;
}
final videoBytes = result.files.first.bytes!;
final fileName = result.files.first.name;
debugPrint('File selected: $fileName (${videoBytes.length} bytes)');
/// Store original video for download/upload
_controller.storeOriginalVideo(videoBytes, fileName);
_currentVideoBytes = videoBytes;
try {
final metadata = await _controller.extractMetadata(videoBytes);
final analysis = _controller.analyzeCompression(metadata);
if (analysis.recommendation != CompressionRecommendation.recommended) {
if (mounted) {
_showRecommendationDialog(analysis);
}
} else {
await _processVideo();
}
} catch (e, stackTrace) {
debugPrint('Error during analysis: $e');
debugPrint('Stack trace: $stackTrace');
_showMessage('Error: $e');
}
}
void _showRecommendationDialog(CompressionAnalysis analysis) {
showDialog(
context: context,
builder: (context) => CompressionRecommendationDialog(
analysis: analysis,
onCompress: _processVideo,
onSkip: () {
_showMessage('Compression skipped. You can still download the original.');
},
),
);
}
Future<void> _processVideo() async {
if (_currentVideoBytes == null) {
_showMessage('No video selected');
return;
}
try {
await _controller.processVideo(_currentVideoBytes!);
} catch (e) {
_showMessage('Processing failed: $e');
}
}
void _showMessage(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: SelectableText(message)),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _controller,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
title: const SelectableText('Video Processor'),
actions: [
/// Clear button
if (_controller.originalMetadata != null)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clearVideos();
_currentVideoBytes = null;
_showMessage('Videos cleared');
},
tooltip: 'Clear videos',
),
],
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStandardsCard(),
const SizedBox(height: 24),
SelectableText(
_controller.statusMessage,
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
if (_controller.isProcessing || _controller.isUploading)
_buildProgressBar(),
const SizedBox(height: 24),
/// Original video section
if (_controller.originalMetadata != null) ...[
_buildOriginalMetadataCard(),
const SizedBox(height: 12),
VideoActionButtons(
label: 'Original',
color: Colors.blue,
onDownload: () {
try {
_controller.downloadOriginal();
_showMessage('Original video downloaded');
} catch (e) {
_showMessage('Download failed: $e');
}
},
onUpload: () async {
try {
final result = await _controller.uploadOriginal();
_showMessage(result.success
? 'Upload successful: ${result.url}'
: 'Upload failed: ${result.errorMessage}');
} catch (e) {
_showMessage('Upload error: $e');
}
},
isUploading: _controller.isUploading,
),
const SizedBox(height: 16),
],
/// Compressed video section
if (_controller.result != null && _controller.result!.success) ...[
_buildResultCard(),
const SizedBox(height: 12),
VideoActionButtons(
label: 'Compressed',
color: Colors.green,
onDownload: () {
try {
_controller.downloadCompressed();
_showMessage('Compressed video downloaded');
} catch (e) {
_showMessage('Download failed: $e');
}
},
onUpload: () async {
try {
final result = await _controller.uploadCompressed();
_showMessage(result.success
? 'Upload successful: ${result.url}'
: 'Upload failed: ${result.errorMessage}');
} catch (e) {
_showMessage('Upload error: $e');
}
},
isUploading: _controller.isUploading,
),
],
const Spacer(),
_buildActionButton(),
],
),
),
);
},
);
}
Widget _buildStandardsCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SelectableText(
'Compression Standards',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
SelectableText('Resolution: ${_controller.compressionConfig.targetHeight}p'),
SelectableText('FPS: ${_controller.compressionConfig.targetFps ?? "original"}'),
SelectableText('Quality (CRF): ${_controller.compressionConfig.crf}'),
SelectableText('Codec: ${_controller.compressionConfig.outputCodec}'),
SelectableText('Format: ${_controller.compressionConfig.outputFormat}'),
],
),
),
);
}
Widget _buildProgressBar() {
return Column(
children: [
LinearProgressIndicator(value: _controller.progress),
const SizedBox(height: 8),
SelectableText('${(_controller.progress * 100).toStringAsFixed(1)}%'),
],
);
}
Widget _buildOriginalMetadataCard() {
final metadata = _controller.originalMetadata!;
return Card(
color: Colors.blue.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SelectableText(
'Original Video',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
SelectableText('Resolution: ${metadata.resolution}'),
SelectableText('FPS: ${metadata.fps.toStringAsFixed(2)}'),
SelectableText('Duration: ${metadata.duration.toStringAsFixed(2)}s'),
SelectableText('Codec: ${metadata.codec}'),
SelectableText('Size: ${metadata.fileSizeMB.toStringAsFixed(2)} MB'),
],
),
),
);
}
Widget _buildResultCard() {
final result = _controller.result!;
return Card(
color: Colors.green.shade50,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SelectableText(
'Compressed Video',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
SelectableText('Resolution: ${result.processedMetadata!.resolution}'),
SelectableText('FPS: ${result.processedMetadata!.fps.toStringAsFixed(2)}'),
SelectableText('Size: ${result.processedMetadata!.fileSizeMB.toStringAsFixed(2)} MB'),
SelectableText('Reduction: ${result.sizeReductionPercent!.toStringAsFixed(1)}%'),
SelectableText('Time: ${result.processingDuration.toStringAsFixed(2)}s'),
],
),
),
);
}
Widget _buildActionButton() {
return ElevatedButton.icon(
onPressed: _controller.isInitialized &&
!_controller.isProcessing &&
!_controller.isUploading
? _pickAndAnalyzeVideo
: null,
icon: const Icon(Icons.video_library),
label: const Text('Select and Analyze Video'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(20),
textStyle: const TextStyle(fontSize: 18),
),
);
}
}