video_web_compressor 0.0.1 copy "video_web_compressor: ^0.0.1" to clipboard
video_web_compressor: ^0.0.1 copied to clipboard

Platformweb

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),
      ),
    );
  }
}
1
likes
150
points
92
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter web package for video compression and format conversion using FFmpeg WebAssembly

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, logger, web

More

Packages that depend on video_web_compressor

Packages that implement video_web_compressor