easy_compressor 1.0.0 copy "easy_compressor: ^1.0.0" to clipboard
easy_compressor: ^1.0.0 copied to clipboard

Cross-platform video compression plugin using native APIs (no FFmpeg). Supports Android, iOS, macOS, and Windows with a simple 0-100 quality parameter.

example/lib/main.dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:easy_compressor/easy_compressor.dart';
import 'package:file_picker/file_picker.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:gal/gal.dart';
import 'package:path/path.dart' as p;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Easy Compressor Demo',
      theme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.deepPurple,
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const CompressorPage(),
    );
  }
}

class CompressorPage extends StatefulWidget {
  const CompressorPage({super.key});

  @override
  State<CompressorPage> createState() => _CompressorPageState();
}

class _CompressorPageState extends State<CompressorPage> {
  final _compressor = EasyCompressor();

  String? _inputPath;
  MediaInfo? _mediaInfo;
  Uint8List? _thumbnail;
  CompressionResult? _result;

  double _quality = 70;
  String _selectedPreset = 'Custom';
  int? _maxHeight;
  int? _frameRate;
  bool _includeAudio = true;
  bool _showAdvanced = false;

  double _progress = 0;
  bool _isCompressing = false;
  String? _error;

  final Map<String, int?> _resolutionOptions = {
    'Original': null,
    '480p': 480,
    '720p': 720,
    '1080p': 1080,
  };

  final Map<String, int?> _fpsOptions = {
    'Original': null,
    '24': 24,
    '30': 30,
    '60': 60,
  };

  Future<void> _pickVideo() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.video,
      allowMultiple: false,
    );

    if (result == null || result.files.isEmpty) return;
    final path = result.files.single.path;
    if (path == null) return;

    setState(() {
      _inputPath = path;
      _mediaInfo = null;
      _thumbnail = null;
      _result = null;
      _error = null;
    });

    try {
      final info = await _compressor.getMediaInfo(path);
      final thumb = await _compressor.getThumbnail(path, maxHeight: 200);
      setState(() {
        _mediaInfo = info;
        _thumbnail = thumb;
      });
    } catch (e) {
      setState(() => _error = 'Failed to read video info: $e');
    }
  }

  void _applyPreset(String preset) {
    setState(() {
      _selectedPreset = preset;
      switch (preset) {
        case 'WhatsApp':
          _quality = 65;
          _maxHeight = 720;
          _frameRate = 30;
          _includeAudio = true;
        case 'Social':
          _quality = 70;
          _maxHeight = 1080;
          _frameRate = 30;
          _includeAudio = true;
        case 'Light':
          _quality = 90;
          _maxHeight = null;
          _frameRate = null;
          _includeAudio = true;
        case 'Maximum':
          _quality = 30;
          _maxHeight = 480;
          _frameRate = 24;
          _includeAudio = true;
      }
    });
  }

  Future<void> _compress() async {
    if (_inputPath == null) return;

    setState(() {
      _isCompressing = true;
      _progress = 0;
      _result = null;
      _error = null;
    });

    try {
      final result = await _compressor.compressVideo(
        _inputPath!,
        config: CompressionConfig(
          quality: _quality.round(),
          maxHeight: _maxHeight,
          frameRate: _frameRate,
          includeAudio: _includeAudio,
        ),
        onProgress: (progress) {
          setState(() => _progress = progress);
        },
      );

      setState(() {
        _result = result;
        _isCompressing = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isCompressing = false;
      });
    }
  }

  Future<void> _cancel() async {
    await _compressor.cancelCompression();
    setState(() => _isCompressing = false);
  }

  void _playVideo(String path, String title) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => VideoPlayerPage(path: path, title: title),
      ),
    );
  }

  void _compareVideos() {
    if (_inputPath != null && _result != null) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => ComparePlayerPage(
            originalPath: _inputPath!,
            compressedPath: _result!.outputPath,
          ),
        ),
      );
    }
  }

  Future<void> _saveVideo() async {
    if (_result == null) return;

    try {
      if (Platform.isAndroid || Platform.isIOS) {
        final hasAccess = await Gal.hasAccess();
        if (!hasAccess) {
          final granted = await Gal.requestAccess();
          if (!granted) {
            if (mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Permission denied to gallery')),
              );
            }
            return;
          }
        }
        await Gal.putVideo(_result!.outputPath);
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Video saved to gallery!')),
          );
        }
      } else {
        // Desktop (macOS, Windows)
        String? selectedPath = await FilePicker.platform.saveFile(
          dialogTitle: 'Save Compressed Video',
          fileName: p.basename(_result!.outputPath),
          type: FileType.video,
        );

        if (selectedPath != null) {
          await File(_result!.outputPath).copy(selectedPath);
          if (mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Saved to $selectedPath')),
            );
          }
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Error saving video: $e')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Easy Compressor'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            FilledButton.icon(
              onPressed: _isCompressing ? null : _pickVideo,
              icon: const Icon(Icons.video_library),
              label: const Text('Pick Video'),
            ),
            const SizedBox(height: 16),

            if (_thumbnail != null || _mediaInfo != null)
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text('Original Video',
                              style: Theme.of(context).textTheme.titleMedium),
                          if (_inputPath != null)
                            IconButton.filledTonal(
                              onPressed: () => _playVideo(_inputPath!, 'Original Video'),
                              icon: const Icon(Icons.play_arrow),
                              tooltip: 'Play Original',
                            ),
                        ],
                      ),
                      const SizedBox(height: 12),
                      if (_thumbnail != null)
                        ClipRRect(
                          borderRadius: BorderRadius.circular(8),
                          child: Image.memory(
                            _thumbnail!,
                            height: 180,
                            width: double.infinity,
                            fit: BoxFit.cover,
                          ),
                        ),
                      if (_mediaInfo != null) ...[
                        const SizedBox(height: 12),
                        _InfoRow('Size', _mediaInfo!.fileSizeFormatted),
                        _InfoRow('Resolution', _mediaInfo!.resolution),
                        _InfoRow('Duration', '${_mediaInfo!.duration.inSeconds}s'),
                        _InfoRow('Frame Rate', '${_mediaInfo!.frameRate.toStringAsFixed(1)} fps'),
                      ],
                    ],
                  ),
                ),
              ),

            if (_inputPath != null) ...[
              const SizedBox(height: 16),
              Card(
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Quality', style: Theme.of(context).textTheme.titleMedium),
                      Slider(
                        value: _quality,
                        min: 0,
                        max: 100,
                        divisions: 100,
                        label: '${_quality.round()}',
                        onChanged: _isCompressing ? null : (v) => setState(() {
                          _quality = v;
                          _selectedPreset = 'Custom';
                        }),
                      ),
                      Center(child: Text('Quality: ${_quality.round()}')),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 12),
              Wrap(
                spacing: 8,
                children: ['WhatsApp', 'Social', 'Light', 'Maximum'].map((p) {
                  return ChoiceChip(
                    label: Text(p),
                    selected: _selectedPreset == p,
                    onSelected: _isCompressing ? null : (_) => _applyPreset(p),
                  );
                }).toList(),
              ),
              const SizedBox(height: 16),
              Card(
                child: Column(
                  children: [
                    ListTile(
                      title: const Text('Advanced Settings'),
                      trailing: Icon(_showAdvanced ? Icons.expand_less : Icons.expand_more),
                      onTap: () => setState(() => _showAdvanced = !_showAdvanced),
                    ),
                    if (_showAdvanced)
                      Padding(
                        padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                        child: Column(
                          children: [
                            DropdownButtonFormField<int?>(
                              value: _maxHeight,
                              decoration: const InputDecoration(
                                labelText: 'Max Resolution',
                                border: OutlineInputBorder(),
                              ),
                              items: _resolutionOptions.entries
                                  .map((e) => DropdownMenuItem(value: e.value, child: Text(e.key)))
                                  .toList(),
                              onChanged: _isCompressing ? null : (v) => setState(() {
                                _maxHeight = v;
                                _selectedPreset = 'Custom';
                              }),
                            ),
                            const SizedBox(height: 12),
                            DropdownButtonFormField<int?>(
                              value: _frameRate,
                              decoration: const InputDecoration(
                                labelText: 'Frame Rate',
                                border: OutlineInputBorder(),
                              ),
                              items: _fpsOptions.entries
                                  .map((e) => DropdownMenuItem(value: e.value, child: Text(e.key)))
                                  .toList(),
                              onChanged: _isCompressing ? null : (v) => setState(() {
                                _frameRate = v;
                                _selectedPreset = 'Custom';
                              }),
                            ),
                            const SizedBox(height: 12),
                            SwitchListTile(
                              title: const Text('Include Audio'),
                              value: _includeAudio,
                              onChanged: _isCompressing ? null : (v) => setState(() {
                                _includeAudio = v;
                                _selectedPreset = 'Custom';
                              }),
                            ),
                          ],
                        ),
                      ),
                  ],
                ),
              ),
              const SizedBox(height: 16),
              if (_isCompressing) ...[
                LinearProgressIndicator(value: _progress),
                const SizedBox(height: 8),
                Text('${(_progress * 100).toStringAsFixed(1)}%', textAlign: TextAlign.center),
                const SizedBox(height: 8),
                OutlinedButton.icon(
                  onPressed: _cancel,
                  icon: const Icon(Icons.cancel),
                  label: const Text('Cancel'),
                ),
              ] else
                FilledButton.icon(
                  onPressed: _compress,
                  icon: const Icon(Icons.compress),
                  label: const Text('Compress Video'),
                ),
            ],

            if (_result != null) ...[
              const SizedBox(height: 24),
              Card(
                color: colorScheme.primaryContainer,
                child: Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Compression Results',
                          style: Theme.of(context).textTheme.titleLarge?.copyWith(
                              color: colorScheme.onPrimaryContainer)),
                      const SizedBox(height: 12),
                      _InfoRow('Compressed Size', _result!.compressedSizeFormatted),
                      _InfoRow('Saved', _result!.spaceSavedFormatted),
                      _InfoRow('Reduction', '${_result!.spaceSavedPercent.toStringAsFixed(1)}%'),
                      const SizedBox(height: 16),
                      Row(
                        children: [
                          Expanded(
                            child: FilledButton.icon(
                              onPressed: () => _playVideo(_result!.outputPath, 'Compressed Video'),
                              icon: const Icon(Icons.play_circle_filled),
                              label: const Text('Play'),
                            ),
                          ),
                          const SizedBox(width: 8),
                          Expanded(
                            child: FilledButton.icon(
                              onPressed: _compareVideos,
                              icon: const Icon(Icons.compare),
                              label: const Text('Compare'),
                            ),
                          ),
                        ],
                      ),
                      const SizedBox(height: 8),
                      FilledButton.icon(
                        onPressed: _saveVideo,
                        icon: const Icon(Icons.download),
                        label: const Text('Save / Download'),
                        style: FilledButton.styleFrom(
                          minimumSize: const Size(double.infinity, 48),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
            const SizedBox(height: 32),
          ],
        ),
      ),
    );
  }
}

class VideoPlayerPage extends StatefulWidget {
  final String path;
  final String title;
  const VideoPlayerPage({super.key, required this.path, required this.title});
  @override
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _videoPlayerController;
  ChewieController? _chewieController;

  @override
  void initState() {
    super.initState();
    _initializePlayer();
  }

  Future<void> _initializePlayer() async {
    _videoPlayerController = VideoPlayerController.file(File(widget.path));
    await _videoPlayerController.initialize();
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController,
      autoPlay: true,
      looping: false,
      aspectRatio: _videoPlayerController.value.aspectRatio,
    );
    setState(() {});
  }

  @override
  void dispose() {
    _videoPlayerController.dispose();
    _chewieController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: _chewieController != null &&
                _chewieController!.videoPlayerController.value.isInitialized
            ? Chewie(controller: _chewieController!)
            : const CircularProgressIndicator(),
      ),
    );
  }
}

class ComparePlayerPage extends StatefulWidget {
  final String originalPath;
  final String compressedPath;
  const ComparePlayerPage({super.key, required this.originalPath, required this.compressedPath});
  @override
  State<ComparePlayerPage> createState() => _ComparePlayerPageState();
}

class _ComparePlayerPageState extends State<ComparePlayerPage> {
  late VideoPlayerController _origController;
  late VideoPlayerController _compController;
  bool _isInitialized = false;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    _origController = VideoPlayerController.file(File(widget.originalPath));
    _compController = VideoPlayerController.file(File(widget.compressedPath));
    await Future.wait([_origController.initialize(), _compController.initialize()]);
    
    // Listeners for UI updates (like play/pause state)
    _origController.addListener(() => setState(() {}));
    _compController.addListener(() => setState(() {}));

    setState(() => _isInitialized = true);
  }

  @override
  void dispose() {
    _origController.dispose();
    _compController.dispose();
    super.dispose();
  }

  void _togglePlay() {
    setState(() {
      if (_origController.value.isPlaying) {
        _origController.pause();
        _compController.pause();
      } else {
        // Sync positions before playing to ensure they are at the exact same spot
        final targetPosition = _origController.value.position;
        _compController.seekTo(targetPosition);
        
        _origController.play();
        _compController.play();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Compare Videos')),
      body: _isInitialized
          ? Column(
              children: [
                Expanded(
                  child: Row(
                    children: [
                      Expanded(
                        child: Column(
                          children: [
                            const Padding(
                              padding: EdgeInsets.all(8.0),
                              child: Text('Original', style: TextStyle(fontWeight: FontWeight.bold)),
                            ),
                            Expanded(child: Center(child: AspectRatio(aspectRatio: _origController.value.aspectRatio, child: VideoPlayer(_origController)))),
                          ],
                        ),
                      ),
                      const VerticalDivider(width: 1),
                      Expanded(
                        child: Column(
                          children: [
                            const Padding(
                              padding: EdgeInsets.all(8.0),
                              child: Text('Compressed', style: TextStyle(fontWeight: FontWeight.bold)),
                            ),
                            Expanded(child: Center(child: AspectRatio(aspectRatio: _compController.value.aspectRatio, child: VideoPlayer(_compController)))),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(24.0),
                  child: Column(
                    children: [
                      VideoProgressIndicator(
                        _origController,
                        allowScrubbing: true,
                        colors: const VideoProgressColors(
                          playedColor: Colors.deepPurple,
                          bufferedColor: Colors.grey,
                          backgroundColor: Colors.black12,
                        ),
                      ),
                      const SizedBox(height: 16),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          IconButton.filled(
                            iconSize: 48,
                            onPressed: _togglePlay,
                            icon: Icon(_origController.value.isPlaying ? Icons.pause : Icons.play_arrow),
                          ),
                          const SizedBox(width: 16),
                          IconButton.outlined(
                            onPressed: () {
                              _origController.seekTo(Duration.zero);
                              _compController.seekTo(Duration.zero);
                              _origController.pause();
                              _compController.pause();
                            },
                            icon: const Icon(Icons.replay),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            )
          : const Center(child: CircularProgressIndicator()),
    );
  }
}

class _InfoRow extends StatelessWidget {
  final String label;
  final String value;
  const _InfoRow(this.label, this.value);
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label, style: const TextStyle(fontWeight: FontWeight.w500)),
          Text(value),
        ],
      ),
    );
  }
}
0
likes
150
points
140
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform video compression plugin using native APIs (no FFmpeg). Supports Android, iOS, macOS, and Windows with a simple 0-100 quality parameter.

Repository (GitHub)
View/report issues
Contributing

Topics

#video #compression #media #video-processing #cross-platform

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on easy_compressor

Packages that implement easy_compressor