lgpl_ffmpeg_flutter 0.0.4 copy "lgpl_ffmpeg_flutter: ^0.0.4" to clipboard
lgpl_ffmpeg_flutter: ^0.0.4 copied to clipboard

Controlled Flutter wrapper around LGPL FFmpeg dynamic libraries for video info and cover extraction.

example/lib/main.dart

import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:lgpl_ffmpeg_flutter/lgpl_ffmpeg_flutter.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
      ),
      home: const VideoProbePage(),
    );
  }
}

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

  @override
  State<VideoProbePage> createState() => _VideoProbePageState();
}

class _VideoProbePageState extends State<VideoProbePage> {
  String? _videoPath;
  VideoInfo? _info;
  FfmpegBackendInfo? _backendInfo;
  String? _coverPath;
  String? _message;
  String? _backendMessage;
  bool _loading = false;

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

  Future<void> _loadBackendInfo() async {
    try {
      final info = await LgplFfmpegFlutter.backendInfo();
      if (!mounted) {
        return;
      }
      setState(() {
        _backendInfo = info;
        _backendMessage = null;
      });
    } on VideoProcessException catch (error) {
      if (!mounted) {
        return;
      }
      setState(() {
        _backendMessage = '${error.code.name}: ${error.message}';
      });
    }
  }

  Future<void> _pickAndProcessVideo() async {
    setState(() {
      _loading = true;
      _message = null;
      _info = null;
      _coverPath = null;
    });

    try {
      final result = await FilePicker.platform.pickFiles(
        type: FileType.video,
        allowMultiple: false,
      );
      final path = result?.files.single.path;
      if (path == null) {
        setState(() {
          _message = 'No video selected.';
        });
        return;
      }

      final info = await LgplFfmpegFlutter.readInfo(videoPath: path);
      final coverPath = await LgplFfmpegFlutter.generateCover(
        videoPath: path,
        preferredTimes: const [Duration(seconds: 1), Duration(seconds: 3)],
        maxLongEdge: 1280,
      );

      setState(() {
        _videoPath = path;
        _info = info;
        _coverPath = coverPath;
      });
    } on VideoProcessException catch (error) {
      setState(() {
        _message = '${error.code.name}: ${error.message}';
      });
    } catch (error) {
      setState(() {
        _message = error.toString();
      });
    } finally {
      if (mounted) {
        setState(() {
          _loading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final info = _info;
    final backendInfo = _backendInfo;
    return Scaffold(
      appBar: AppBar(title: const Text('lgpl_ffmpeg_flutter')),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          FilledButton.icon(
            onPressed: _loading ? null : _pickAndProcessVideo,
            icon: _loading
                ? const SizedBox.square(
                    dimension: 18,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.video_file_outlined),
            label: Text(_loading ? 'Processing...' : 'Pick video'),
          ),
          const SizedBox(height: 20),
          if (backendInfo != null) ...[
            _InfoRow(label: 'FFmpeg', value: backendInfo.ffmpegVersion),
            _InfoRow(label: 'FFmpeg license', value: backendInfo.license),
            _InfoRow(label: 'FFmpeg config', value: backendInfo.configuration),
            const SizedBox(height: 8),
          ] else if (_backendMessage != null) ...[
            _InfoRow(label: 'FFmpeg backend', value: _backendMessage!),
            const SizedBox(height: 8),
          ],
          if (_message != null) Text(_message!),
          if (_videoPath != null)
            _InfoRow(label: 'Video path', value: _videoPath!),
          if (info != null) ...[
            _InfoRow(label: 'Duration', value: info.duration.toString()),
            _InfoRow(label: 'Size', value: '${info.width} x ${info.height}'),
            _InfoRow(label: 'Rotation', value: '${info.rotation}'),
            _InfoRow(label: 'Bitrate', value: '${info.bitrate ?? '-'}'),
            _InfoRow(label: 'MIME type', value: info.mimeType),
            _InfoRow(label: 'Format', value: info.formatName ?? '-'),
            _InfoRow(label: 'Video codec', value: info.videoCodec ?? '-'),
            _InfoRow(label: 'Audio codec', value: info.audioCodec ?? '-'),
            _InfoRow(
              label: 'File size',
              value: info.fileSizeBytes == null
                  ? '-'
                  : '${info.fileSizeBytes} bytes',
            ),
          ],
          if (_coverPath != null) ...[
            _CoverPreview(coverPath: _coverPath!),
            const SizedBox(height: 16),
            _InfoRow(label: 'Cover path', value: _coverPath!),
          ],
        ],
      ),
    );
  }
}

class _CoverPreview extends StatelessWidget {
  const _CoverPreview({required this.coverPath});

  final String coverPath;

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Cover', style: Theme.of(context).textTheme.labelLarge),
        const SizedBox(height: 8),
        ConstrainedBox(
          constraints: const BoxConstraints(maxHeight: 360),
          child: AspectRatio(
            aspectRatio: 16 / 9,
            child: Material(
              clipBehavior: Clip.antiAlias,
              color: colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(8),
              child: InkWell(
                onTap: () => _openCoverViewer(context, coverPath),
                child: Stack(
                  fit: StackFit.expand,
                  children: [
                    Image.file(
                      imageFile,
                      fit: BoxFit.contain,
                      errorBuilder: (context, error, stackTrace) {
                        return Center(
                          child: Padding(
                            padding: const EdgeInsets.all(20),
                            child: Text(
                              'Unable to load cover image.',
                              style: TextStyle(color: colorScheme.error),
                              textAlign: TextAlign.center,
                            ),
                          ),
                        );
                      },
                    ),
                    Positioned(
                      right: 8,
                      bottom: 8,
                      child: DecoratedBox(
                        decoration: BoxDecoration(
                          color: Colors.black.withValues(alpha: 0.54),
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: const Padding(
                          padding: EdgeInsets.all(8),
                          child: Icon(
                            Icons.fullscreen,
                            color: Colors.white,
                            size: 22,
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }

  void _openCoverViewer(BuildContext context, String coverPath) {
    Navigator.of(context).push<void>(
      MaterialPageRoute<void>(
        builder: (_) => _CoverViewerPage(coverPath: coverPath),
      ),
    );
  }
}

class _CoverViewerPage extends StatelessWidget {
  const _CoverViewerPage({required this.coverPath});

  final String coverPath;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        backgroundColor: Colors.black,
        foregroundColor: Colors.white,
        title: const Text('Cover'),
      ),
      body: SafeArea(
        child: Center(
          child: InteractiveViewer(
            minScale: 0.5,
            maxScale: 5,
            child: Image.file(
              File(coverPath),
              fit: BoxFit.contain,
              errorBuilder: (context, error, stackTrace) {
                return const Padding(
                  padding: EdgeInsets.all(24),
                  child: Text(
                    'Unable to load cover image.',
                    style: TextStyle(color: Colors.white),
                    textAlign: TextAlign.center,
                  ),
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

class _InfoRow extends StatelessWidget {
  const _InfoRow({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(label, style: Theme.of(context).textTheme.labelLarge),
          const SizedBox(height: 4),
          SelectableText(value),
        ],
      ),
    );
  }
}
1
likes
0
points
329
downloads

Publisher

unverified uploader

Weekly Downloads

Controlled Flutter wrapper around LGPL FFmpeg dynamic libraries for video info and cover extraction.

Repository (GitHub)
View/report issues

Topics

#ffmpeg #video #flutter-plugin #lgpl

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on lgpl_ffmpeg_flutter

Packages that implement lgpl_ffmpeg_flutter