ffmpeg_kit_extended_flutter 0.3.4 copy "ffmpeg_kit_extended_flutter: ^0.3.4" to clipboard
ffmpeg_kit_extended_flutter: ^0.3.4 copied to clipboard

A comprehensive Flutter plugin for executing FFmpeg, FFprobe, and FFplay commands using FFmpeg 8.0 API. Supports Android, Windows, and Linux.

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:ffmpeg_kit_extended_flutter/ffmpeg_kit_extended_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:android_media_store/android_media_store.dart';
import 'package:path_provider/path_provider.dart'; // Import for getTemporaryDirectory

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FFmpegKitExtended.initialize();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FFmpeg Kit Extended Demo',
      theme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  final TextEditingController _outputController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  final TextEditingController _ffmpegCommandController =
      TextEditingController(text: "-version");
  final TextEditingController _ffprobeCommandController =
      TextEditingController(text: "-version");
  final TextEditingController _ffplayCommandController =
      TextEditingController(text: "-i test_video.mp4");
  String? _selectedProbePath;
  String _status = 'Ready';
  final _mediaStore = AndroidMediaStore.instance;
  StreamSubscription<bool>? _permissionStreamSub;
  LogLevel _currentLogLevel = LogLevel.info;

  // FFplay position tracking
  StreamSubscription<double>? _positionSub;
  double _playbackPosition = 0.0;
  bool _hasActiveSession = false;

  // Volume control
  double _volume = 0.5;

  // Video dimensions updated via videoSizeStream when first frame is decoded
  StreamSubscription<(int, int)>? _videoSizeSub;
  int _videoWidth = 0;
  int _videoHeight = 0;
  bool _hasVideo = false;

  // Unified video surface for Android, Linux, Windows
  FFplaySurface? _surface;

  @override
  void dispose() {
    _permissionStreamSub?.cancel();
    _positionSub?.cancel();
    _videoSizeSub?.cancel();
    _surface?.release();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    _initializePlugin();
    _currentLogLevel = FFmpegKitConfig.getLogLevel();
    final tabController = TabController(length: 3, vsync: this);
    if (Platform.isAndroid) {
      // Listen for MANAGE_MEDIA permission changes.
      _permissionStreamSub =
          _mediaStore.onManageMediaPermissionChanged.listen((isGranted) {
        if (mounted) {
          setState(() {
            _status = isGranted
                ? 'Manage Media Permission: Granted'
                : 'Manage Media Permission: Denied';
            _addLog(_status);
          });
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(_status)),
          );
        }
      });
    }
    _tabController = tabController;
  }

  Future<void> _initializePlugin() async {
    if (!Platform.isAndroid) return;
    try {
      await AndroidMediaStore.ensureInitialized();
      setState(() {
        _status = 'Plugin initialized successfully';
        _addLog(_status);
      });
      await _checkPermissions(silent: true);
    } catch (e) {
      setState(() {
        _status = 'Initialization failed: $e';
        _addLog(_status);
      });
    }
  }

  Future<void> _checkPermissions({bool silent = false}) async {
    if (!Platform.isAndroid) return;
    if (!silent) {
      setState(() {
        _status = 'Checking permissions...';
        _addLog(_status);
      });
    }
    try {
      // Check standard storage/media permissions.
      await [
        Permission.photos,
        Permission.audio,
        Permission.videos,
        Permission.storage,
      ].request();

      // Check Android 12+ Manage Media Access.
      bool canManageMedia = await _mediaStore.canManageMedia();

      if (!canManageMedia) {
        setState(() {
          _status = 'Missing Manage Media Permission';
          _addLog(_status);
        });
        await _mediaStore
            .requestManageMedia(); // Will trigger the stream when user returns
      } else {
        setState(() {
          _status = 'All permissions look good!';
          _addLog(_status);
        });
      }
    } catch (e) {
      setState(() {
        _status = 'Permission check error: $e';
        _addLog(_status);
      });
    }
  }

  void _addLog(String log) {
    setState(() {
      _outputController.text += "$log\n";
    });
    // Scroll to bottom
    Future.delayed(const Duration(milliseconds: 100), () {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOut,
        );
      }
    });
  }

  void _clearLogs() {
    setState(() {
      _outputController.clear();
    });
  }

  // --- FFmpeg Examples ---

  Future<void> _runFFmpegVersion() async {
    _addLog("--- Running FFmpeg -version (Async) ---");

    // Async execution captures logs in real-time.
    await FFmpegKit.executeAsync("-version", onLog: (log) {
      _addLog(log.message);
    }, onComplete: (session) {
      _addLog("Return code: ${session.getReturnCode()}");
    });
  }

  void _runFFmpegInfoSync() {
    _addLog("--- Running FFmpeg -version (Sync) ---");
    // Synchronous execution blocks the current isolate.
    final session = FFmpegKit.execute("-version");
    final output = session.getOutput();

    _addLog("Output captured from sync session:");
    _addLog(output ?? "No output captured.");
    _addLog("Return code: ${session.getReturnCode()}");
  }

  Future<void> _generateTestVideo() async {
    // Use temporary directory for FFmpeg output.
    final tempDir = await getTemporaryDirectory();
    final tempOutputPath = path.join(tempDir.path, 'test_video.mp4');
    _addLog(
        "--- Generating Test Video with Audio to temporary path: $tempOutputPath ---");

    // Command with both video and audio streams
    const command =
        "-hide_banner -loglevel info -f lavfi -i testsrc=duration=5:size=512x512:rate=30 -f lavfi -i sine=frequency=1000:duration=5 -c:v mpeg2video -c:a aac -shortest -y";

    await FFmpegKit.executeAsync("$command \"$tempOutputPath\"", onLog: (log) {
      _addLog(log.message);
    }, onComplete: (session) {
      if (ReturnCode.isSuccess(session.getReturnCode())) {
        _addLog("✅ Video with audio generated successfully!");
      } else {
        _addLog("❌ Generation failed. Code: ${session.getReturnCode()}");
      }
    });
  }

  Future<void> _generateTestAudio() async {
    final tempDir = await getTemporaryDirectory();
    final outputPath = path.join(tempDir.path, 'test_audio.wav');
    _addLog("--- Generating Test Audio to: $outputPath ---");

    // Command from integration tests
    const command =
        "-hide_banner -loglevel info -f lavfi -i sine=frequency=1000:duration=10 -y";

    await FFmpegKit.executeAsync("$command \"$outputPath\"", onLog: (log) {
      _addLog(log.message);
    }, onComplete: (session) {
      if (ReturnCode.isSuccess(session.getReturnCode())) {
        _addLog("✅ Audio generated successfully!");
      } else {
        _addLog("❌ Generation failed. Code: ${session.getReturnCode()}");
      }
    });
  }

  Future<void> _showSystemInfo() async {
    _addLog("--- System & Config Information ---");
    _addLog("FFmpeg Version: ${FFmpegKitConfig.getFFmpegVersion()}");
    _addLog("FFmpegKit Version: ${FFmpegKitConfig.getVersion()}");
    _addLog("Build Date: ${FFmpegKitConfig.getBuildDate()}");
    _addLog("Package Name: ${FFmpegKitConfig.getPackageName()}");
    _addLog(
        "Log Level: ${FFmpegKitConfig.logLevelToString(FFmpegKitConfig.getLogLevel())}");
  }

  // Introspection API methods
  Future<void> _showFFmpegVersion() async {
    _addLog("--- FFmpeg Version ---");
    _addLog(FFmpegKitExtended.getFFmpegVersion());
  }

  Future<void> _showFFmpegArchitecture() async {
    _addLog("--- FFmpeg Architecture ---");
    _addLog(FFmpegKitExtended.getFFmpegArchitecture());
  }

  Future<void> _showFFmpegKitVersion() async {
    _addLog("--- FFmpegKit Version ---");
    _addLog(FFmpegKitExtended.getVersion());
  }

  Future<void> _showPackageName() async {
    _addLog("--- Package Name ---");
    _addLog(FFmpegKitExtended.getPackageName());
  }

  Future<void> _showExternalLibraries() async {
    _addLog("--- External Libraries ---");
    final libraries = FFmpegKitExtended.getExternalLibraries();
    if (libraries.isEmpty) {
      _addLog("No external libraries bundled");
    } else {
      _addLog(libraries);
    }
  }

  Future<void> _showBundleType() async {
    _addLog("--- Bundle Type ---");
    _addLog(FFmpegKitExtended.getBundleType());
  }

  Future<void> _showGplStatus() async {
    _addLog("--- GPL Status ---");
    _addLog(FFmpegKitExtended.isGpl() ? "GPL enabled" : "GPL disabled");
  }

  Future<void> _showNonfreeStatus() async {
    _addLog("--- Non-Free Status ---");
    _addLog(FFmpegKitExtended.isNonfree()
        ? "Non-free enabled"
        : "Non-free disabled");
  }

  Future<void> _showRegisteredCodecs() async {
    _addLog("--- Registered Codecs ---");
    final codecs = FFmpegKitExtended.getRegisteredCodecs();
    _addLog(codecs.isEmpty ? "No codecs found" : codecs);
  }

  Future<void> _showRegisteredEncoders() async {
    _addLog("--- Registered Encoders ---");
    final encoders = FFmpegKitExtended.getRegisteredEncoders();
    _addLog(encoders.isEmpty ? "No encoders found" : encoders);
  }

  Future<void> _showRegisteredDecoders() async {
    _addLog("--- Registered Decoders ---");
    final decoders = FFmpegKitExtended.getRegisteredDecoders();
    _addLog(decoders.isEmpty ? "No decoders found" : decoders);
  }

  Future<void> _showRegisteredMuxers() async {
    _addLog("--- Registered Muxers ---");
    final muxers = FFmpegKitExtended.getRegisteredMuxers();
    _addLog(muxers.isEmpty ? "No muxers found" : muxers);
  }

  Future<void> _showRegisteredDemuxers() async {
    _addLog("--- Registered Demuxers ---");
    final demuxers = FFmpegKitExtended.getRegisteredDemuxers();
    _addLog(demuxers.isEmpty ? "No demuxers found" : demuxers);
  }

  Future<void> _showRegisteredFilters() async {
    _addLog("--- Registered Filters ---");
    final filters = FFmpegKitExtended.getRegisteredFilters();
    _addLog(filters.isEmpty ? "No filters found" : filters);
  }

  Future<void> _showRegisteredProtocols() async {
    _addLog("--- Registered Protocols ---");
    final protocols = FFmpegKitExtended.getRegisteredProtocols();
    _addLog(protocols.isEmpty ? "No protocols found" : protocols);
  }

  Future<void> _showRegisteredBitstreamFilters() async {
    _addLog("--- Registered Bitstream Filters ---");
    final bitstreamFilters = FFmpegKitExtended.getRegisteredBitstreamFilters();
    _addLog(bitstreamFilters.isEmpty
        ? "No bitstream filters found"
        : bitstreamFilters);
  }

  Future<void> _showBuildConfiguration() async {
    _addLog("--- Build Configuration ---");
    final config = FFmpegKitExtended.getBuildConfiguration();
    _addLog(config.isEmpty ? "No build configuration found" : config);
  }

  Future<void> _showBuildDate() async {
    _addLog("--- Build Date ---");
    _addLog(FFmpegKitExtended.getBuildDate());
  }

  void _setLogLevel(LogLevel level) {
    setState(() {
      _currentLogLevel = level;
    });
    FFmpegKitConfig.setLogLevel(level);
    _addLog("Log level set to: ${FFmpegKitConfig.logLevelToString(level)}");
  }

  Future<void> _runCustomFFmpeg() async {
    final command = _ffmpegCommandController.text;
    _addLog("--- Running Custom FFmpeg: $command ---");
    await FFmpegKit.executeAsync(command, onLog: (log) {
      _addLog(log.message);
    }, onComplete: (session) {
      _addLog("Return code: ${session.getReturnCode()}");
    });
  }

  // --- FFprobe Examples ---

  Future<void> _runFFprobeVersion() async {
    _addLog("--- Running FFprobe -version (Async) ---");
    await FFprobeKit.executeAsync("-version", onComplete: (session) {
      final output = session.getOutput();
      _addLog(output ?? "No output found in session object.");
      _addLog("Return code: ${session.getReturnCode()}");
    });
  }

  void _runFFprobeInfoSync() {
    _addLog("--- Running FFprobe -version (Sync) ---");
    // Capturing output from synchronous ffprobe call.
    final session = FFprobeKit.execute("-version");
    final output = session.getOutput();

    _addLog("Output captured from sync ffprobe:");
    _addLog(output ?? "No output captured.");
    _addLog("Return code: ${session.getReturnCode()}");
  }

  Future<void> _pickProbeFile() async {
    final result = await FilePicker.platform.pickFiles();
    if (result != null && result.files.single.path != null) {
      setState(() {
        _selectedProbePath = result.files.single.path;
      });
      _addLog("Selected for probe: $_selectedProbePath");
    }
  }

  Future<void> _runMediaInformation() async {
    // Use picked file, local test video, or remote URL fallback.
    final tempDir = await getTemporaryDirectory();
    final localTestPath = path.join(tempDir.path, 'test_video.mp4');

    final String probePath;
    if (_selectedProbePath != null && File(_selectedProbePath!).existsSync()) {
      probePath = _selectedProbePath!;
    } else if (File(localTestPath).existsSync()) {
      probePath = localTestPath;
    } else {
      probePath =
          "https://raw.githubusercontent.com/tanersener/ffmpeg-kit/master/test-data/video.mp4";
    }

    _addLog("--- Getting Media Information for $probePath ---");

    await FFprobeKit.getMediaInformationAsync(probePath, onComplete: (session) {
      if (session.isMediaInformationSession()) {
        final mediaInfoSession = session as MediaInformationSession;
        final info = mediaInfoSession.getMediaInformation();
        if (info != null) {
          _addLog("Format: ${info.format}");
          _addLog("Duration: ${info.duration}s");
          _addLog("Bitrate: ${info.bitrate}");
          _addLog("Streams count: ${info.streams.length}");
          _addLog("Media Information: ${info.allPropertiesJson}");
          for (var i = 0; i < info.streams.length; i++) {
            final stream = info.streams[i];
            _addLog(
                " Stream #$i: ${stream.type} (${stream.codec}) - ${stream.width}x${stream.height}");
          }
        } else {
          _addLog("Failed to retrieve media information. Check logs below:");
          _addLog(session.getLogs() ?? "Empty logs.");
        }
      }
    });
  }

  Future<void> _runCustomFFprobe() async {
    final command = _ffprobeCommandController.text;
    _addLog("--- Running Custom FFprobe: $command ---");
    await FFprobeKit.executeAsync(command, onComplete: (session) {
      final output = session.getOutput();
      if (output != null) _addLog(output);
      _addLog("Return code: ${session.getReturnCode()}");
    });
  }

  // --- FFplay Example ---

  Future<void> _prepareSurface() async {
    final old = _surface;
    if (mounted) {
      setState(() {
        _surface = null;
        _hasVideo = false;
      });
    }
    await old?.release();
  }

  void _attachPositionStream(FFplaySession session) {
    _positionSub?.cancel();
    _videoSizeSub?.cancel();
    setState(() {
      _hasActiveSession = true;
      _playbackPosition = 0.0;
      _videoWidth = 0;
      _videoHeight = 0; // Reset until actual dimensions arrive
      _hasVideo = false; // Unknown until first frame arrives
      _volume = session.getVolume(); // Sync volume with current session
      _addLog("Volume: $_volume");
    });

    // Capture surface at subscription time to avoid race conditions.
    final surfaceToRelease = _surface;

    _positionSub = session.positionStream.listen(
      (pos) {
        if (mounted) {
          setState(() {
            _playbackPosition = pos;
          });
        }
      },
      onDone: () {
        if (mounted) setState(() => _hasActiveSession = false);
        surfaceToRelease?.release().then((_) {
          if (mounted) {
            setState(() {
              // Only clear if this is still the current surface
              if (_surface == surfaceToRelease) {
                _surface = null;
              }
            });
          }
        });
      },
    );
    _videoSizeSub = session.videoSizeStream.listen((size) {
      final (w, h) = size;
      if (mounted && w > 0 && h > 0) {
        setState(() {
          _videoWidth = w;
          _videoHeight = h;
          _hasVideo = true;
        });
      }
    });
  }

  Future<void> _runFFplay(String fileName) async {
    final tempDir = await getTemporaryDirectory();
    final localPath = path.join(tempDir.path, fileName);

    if (!File(localPath).existsSync()) {
      _addLog("⚠️ File not found: $localPath. Please generate it first!");
      return;
    }

    _addLog("--- Starting FFplay for $localPath ---");

    // Clear existing surface and create new one.
    await _prepareSurface();
    final surface = await FFplaySurface.create();
    if (surface != null && mounted) {
      setState(() => _surface = surface);
    }

    final session = await FFplayKit.executeAsync("-i \"$localPath\"",
        onComplete: (session) {
      _addLog("FFplay playback of $fileName finished");
    });
    _attachPositionStream(session);
    _addLog("Playback started.");
  }

  Future<void> _runCustomFFplay() async {
    final command = _ffplayCommandController.text;
    _addLog("--- Running Custom FFplay: $command ---");

    await _prepareSurface();
    final surface = await FFplaySurface.create();
    if (surface != null && mounted) {
      setState(() => _surface = surface);
    }

    final session =
        await FFplayKit.executeAsync(command, onComplete: (session) {
      _addLog("FFplay playback finished");
    });
    _attachPositionStream(session);
  }

  void _seekForward() {
    if (_hasActiveSession) {
      final newPosition = FFplayKit.position + 1.0;
      FFplayKit.seek(newPosition);
    }
  }

  void _seekBackward() {
    if (_hasActiveSession) {
      final newPosition =
          (FFplayKit.position - 1.0).clamp(0.0, double.infinity);
      FFplayKit.seek(newPosition);
    }
  }

  void _setVolume(double volume) {
    if (_hasActiveSession) {
      final session = FFplayKit.getCurrentSession();
      if (session != null) {
        session.setVolume(volume);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final isMobile = constraints.maxWidth < 600;

      return Scaffold(
        appBar: AppBar(
          title: Text(isMobile ? 'FFmpeg Kit' : 'FFmpeg Kit Extended'),
          bottom: TabBar(
            controller: _tabController,
            tabs: [
              Tab(
                  icon: const Icon(Icons.movie),
                  text: isMobile ? null : "FFmpeg"),
              Tab(
                  icon: const Icon(Icons.info),
                  text: isMobile ? null : "FFprobe"),
              Tab(
                  icon: const Icon(Icons.play_arrow),
                  text: isMobile ? null : "FFplay"),
            ],
          ),
          actions: [
            PopupMenuButton<LogLevel>(
              icon: const Icon(Icons.tune),
              tooltip: "Log Level",
              onSelected: _setLogLevel,
              itemBuilder: (BuildContext context) {
                return LogLevel.values.map((LogLevel level) {
                  return PopupMenuItem<LogLevel>(
                    value: level,
                    child: Row(
                      children: [
                        Icon(
                          _currentLogLevel == level
                              ? Icons.radio_button_checked
                              : Icons.radio_button_unchecked,
                          size: 16,
                        ),
                        const SizedBox(width: 8),
                        Text(level.name.toUpperCase()),
                      ],
                    ),
                  );
                }).toList();
              },
            ),
            PopupMenuButton<String>(
              icon: const Icon(Icons.settings),
              tooltip: "System Info",
              onSelected: (String value) {
                switch (value) {
                  case 'basic_info':
                    _showSystemInfo();
                    break;
                  case 'ffmpeg_version':
                    _showFFmpegVersion();
                    break;
                  case 'ffmpeg_arch':
                    _showFFmpegArchitecture();
                    break;
                  case 'ffmpegkit_version':
                    _showFFmpegKitVersion();
                    break;
                  case 'package_name':
                    _showPackageName();
                    break;
                  case 'external_libs':
                    _showExternalLibraries();
                    break;
                  case 'bundle_type':
                    _showBundleType();
                    break;
                  case 'gpl_status':
                    _showGplStatus();
                    break;
                  case 'nonfree_status':
                    _showNonfreeStatus();
                    break;
                  case 'codecs':
                    _showRegisteredCodecs();
                    break;
                  case 'encoders':
                    _showRegisteredEncoders();
                    break;
                  case 'decoders':
                    _showRegisteredDecoders();
                    break;
                  case 'muxers':
                    _showRegisteredMuxers();
                    break;
                  case 'demuxers':
                    _showRegisteredDemuxers();
                    break;
                  case 'filters':
                    _showRegisteredFilters();
                    break;
                  case 'protocols':
                    _showRegisteredProtocols();
                    break;
                  case 'bitstream_filters':
                    _showRegisteredBitstreamFilters();
                    break;
                  case 'build_config':
                    _showBuildConfiguration();
                    break;
                  case 'build_date':
                    _showBuildDate();
                    break;
                }
              },
              itemBuilder: (BuildContext context) {
                return [
                  const PopupMenuItem<String>(
                    value: 'basic_info',
                    child: Row(
                      children: [
                        Icon(Icons.info_outline),
                        SizedBox(width: 8),
                        Text('Basic System Info'),
                      ],
                    ),
                  ),
                  const PopupMenuDivider(),
                  const PopupMenuItem<String>(
                    value: 'ffmpeg_version',
                    child: Row(
                      children: [
                        Icon(Icons.verified),
                        SizedBox(width: 8),
                        Text('FFmpeg Version'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'ffmpeg_arch',
                    child: Row(
                      children: [
                        Icon(Icons.memory),
                        SizedBox(width: 8),
                        Text('FFmpeg Architecture'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'ffmpegkit_version',
                    child: Row(
                      children: [
                        Icon(Icons.info),
                        SizedBox(width: 8),
                        Text('FFmpegKit Version'),
                      ],
                    ),
                  ),
                  PopupMenuItem<String>(
                    value: 'package_name',
                    child: Row(
                      children: [
                        Icon(Icons.inventory_2_outlined),
                        SizedBox(width: 8),
                        Text('Package Name'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'external_libs',
                    child: Row(
                      children: [
                        Icon(Icons.library_books),
                        SizedBox(width: 8),
                        Text('External Libraries'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'bundle_type',
                    child: Row(
                      children: [
                        Icon(Icons.category),
                        SizedBox(width: 8),
                        Text('Bundle Type'),
                      ],
                    ),
                  ),
                  const PopupMenuDivider(),
                  const PopupMenuItem<String>(
                    value: 'gpl_status',
                    child: Row(
                      children: [
                        Icon(Icons.gavel),
                        SizedBox(width: 8),
                        Text('GPL Status'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'nonfree_status',
                    child: Row(
                      children: [
                        Icon(Icons.lock),
                        SizedBox(width: 8),
                        Text('Non-Free Status'),
                      ],
                    ),
                  ),
                  const PopupMenuDivider(),
                  const PopupMenuItem<String>(
                    value: 'codecs',
                    child: Row(
                      children: [
                        Icon(Icons.video_settings),
                        SizedBox(width: 8),
                        Text('Registered Codecs'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'encoders',
                    child: Row(
                      children: [
                        Icon(Icons.upload),
                        SizedBox(width: 8),
                        Text('Registered Encoders'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'decoders',
                    child: Row(
                      children: [
                        Icon(Icons.download),
                        SizedBox(width: 8),
                        Text('Registered Decoders'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'muxers',
                    child: Row(
                      children: [
                        Icon(Icons.merge_type),
                        SizedBox(width: 8),
                        Text('Registered Muxers'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'demuxers',
                    child: Row(
                      children: [
                        Icon(Icons.call_split),
                        SizedBox(width: 8),
                        Text('Registered Demuxers'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'filters',
                    child: Row(
                      children: [
                        Icon(Icons.filter_alt),
                        SizedBox(width: 8),
                        Text('Registered Filters'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'protocols',
                    child: Row(
                      children: [
                        Icon(Icons.link),
                        SizedBox(width: 8),
                        Text('Registered Protocols'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'bitstream_filters',
                    child: Row(
                      children: [
                        Icon(Icons.tune),
                        SizedBox(width: 8),
                        Text('Registered Bitstream Filters'),
                      ],
                    ),
                  ),
                  const PopupMenuDivider(),
                  const PopupMenuItem<String>(
                    value: 'build_config',
                    child: Row(
                      children: [
                        Icon(Icons.build),
                        SizedBox(width: 8),
                        Text('Build Configuration'),
                      ],
                    ),
                  ),
                  const PopupMenuItem<String>(
                    value: 'build_date',
                    child: Row(
                      children: [
                        Icon(Icons.date_range),
                        SizedBox(width: 8),
                        Text('Build Date'),
                      ],
                    ),
                  ),
                ];
              },
            ),
            IconButton(
              icon: const Icon(Icons.delete),
              onPressed: _clearLogs,
              tooltip: "Clear Logs",
            ),
            IconButton(
              icon: const Icon(Icons.verified_user),
              onPressed: _checkPermissions,
              tooltip: "Check / Request Permissions",
            )
          ],
        ),
        body: Column(
          children: [
            Expanded(flex: isMobile ? 3 : 2, child: _buildTabBarView()),
            const Divider(height: 1),
            Expanded(flex: isMobile ? 2 : 1, child: _buildLogView()),
          ],
        ),
      );
    });
  }

  Widget _buildTabBarView() {
    return TabBarView(
      controller: _tabController,
      children: [
        _buildFFmpegTab(),
        _buildFFprobeTab(),
        _buildFFplayTab(),
      ],
    );
  }

  Widget _buildLogView() {
    return Container(
      color: Colors.black,
      padding: const EdgeInsets.all(8.0),
      child: SingleChildScrollView(
        controller: _scrollController,
        child: TextField(
          controller: _outputController,
          maxLines: null,
          readOnly: true,
          style: const TextStyle(
            color: Colors.greenAccent,
            fontFamily: 'monospace',
            fontSize: 12,
          ),
          decoration: const InputDecoration(
            border: InputBorder.none,
          ),
        ),
      ),
    );
  }

  Widget _buildFFmpegTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: [
              _demoButton(_generateTestVideo, Icons.video_call, "Gen Video"),
              _demoButton(_generateTestAudio, Icons.audiotrack, "Gen Audio"),
              _demoButton(_runFFmpegVersion, Icons.bolt, "Async Version"),
              _demoButton(_runFFmpegInfoSync, Icons.timer, "Sync Version"),
              _demoButton(() async {
                _addLog("--- Running Help ---");
                await FFmpegKit.executeAsync("-h",
                    onLog: (l) => _addLog(l.message));
              }, Icons.help_outline, "Help"),
            ],
          ),
          const SizedBox(height: 24),
          _buildCustomCommandSection(_ffmpegCommandController, _runCustomFFmpeg,
              "Enter FFmpeg command"),
        ],
      ),
    );
  }

  Widget _buildFFprobeTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (_selectedProbePath != null) ...[
            Text("Selected: ${path.basename(_selectedProbePath!)}",
                style:
                    const TextStyle(fontSize: 12, fontStyle: FontStyle.italic)),
            const SizedBox(height: 8),
          ],
          Wrap(
            spacing: 12,
            runSpacing: 12,
            children: [
              _demoButton(_pickProbeFile, Icons.file_open, "Pick File"),
              _demoButton(
                  _runMediaInformation, Icons.analytics, "Get Media Info"),
              _demoButton(_runFFprobeVersion, Icons.bolt, "Async Version"),
              _demoButton(_runFFprobeInfoSync, Icons.timer, "Sync Version"),
            ],
          ),
          const SizedBox(height: 24),
          _buildCustomCommandSection(_ffprobeCommandController,
              _runCustomFFprobe, "Enter FFprobe command"),
        ],
      ),
    );
  }

  Widget _buildFFplayTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (_hasVideo && _surface != null) ...[
            LayoutBuilder(
              builder: (context, constraints) {
                final srcW = _videoWidth.toDouble();
                final srcH = _videoHeight.toDouble();
                final availW = constraints.maxWidth;
                final scale = (srcW > availW && srcW > 0) ? availW / srcW : 1.0;
                return Center(
                  child: SizedBox(
                    width: srcW * scale,
                    height: srcH * scale,
                    child: _surface!.toWidget(),
                  ),
                );
              },
            ),
            const SizedBox(height: 12),
          ],
          _buildCustomCommandSection(_ffplayCommandController, _runCustomFFplay,
              "Enter FFplay command"),
          const SizedBox(height: 20),
          const Text("1. Generate Media:",
              style: TextStyle(fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          Wrap(
            spacing: 10,
            children: [
              _demoButton(_generateTestVideo, Icons.video_call, "Gen Video"),
              _demoButton(_generateTestAudio, Icons.audiotrack, "Gen Audio"),
            ],
          ),
          const SizedBox(height: 20),
          const Text("2. Play Generated:",
              style: TextStyle(fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          Wrap(
            spacing: 10,
            children: [
              _demoButton(() => _runFFplay('test_video.mp4'),
                  Icons.play_circle_filled, "Play Video"),
              _demoButton(() => _runFFplay('test_audio.wav'), Icons.music_note,
                  "Play Audio"),
            ],
          ),
          const SizedBox(height: 20),
          const Text("Controls:",
              style: TextStyle(fontWeight: FontWeight.bold)),
          Row(
            children: [
              IconButton(
                  onPressed: () => FFplayKit.pause(),
                  icon: const Icon(Icons.pause)),
              IconButton(
                  onPressed: () => FFplayKit.resume(),
                  icon: const Icon(Icons.play_arrow)),
              IconButton(
                  onPressed: () => FFplayKit.stop(),
                  icon: const Icon(Icons.stop)),
              IconButton(
                  onPressed: () => _seekForward(),
                  icon: const Icon(Icons.fast_forward)),
              IconButton(
                  onPressed: () => _seekBackward(),
                  icon: const Icon(Icons.fast_rewind)),
            ],
          ),
          const SizedBox(height: 8),
          if (_hasActiveSession) ...[
            Text(
                "State: ${FFplayKit.playing ? 'Playing' : (FFplayKit.paused ? 'Paused' : 'Stopped')}"),
            Text(
                "Position: ${_playbackPosition.toStringAsFixed(1)}s / ${FFplayKit.duration.toStringAsFixed(1)}s"),
            Slider(
              value: (_playbackPosition /
                      (FFplayKit.duration > 0 ? FFplayKit.duration : 1.0))
                  .clamp(0.0, 1.0),
              onChanged: (val) => FFplayKit.seek(val * FFplayKit.duration),
            ),
            const SizedBox(height: 16),
            const Text("Volume:",
                style: TextStyle(fontWeight: FontWeight.bold)),
            Row(
              children: [
                const Icon(Icons.volume_down),
                Expanded(
                  child: Slider(
                    value: _volume,
                    min: 0.0,
                    max: 1.0,
                    divisions: 20,
                    onChanged: (val) {
                      _addLog("Current volume: $_volume, new volume: $val");
                      setState(() {
                        _volume = val;
                      });
                      _setVolume(val);
                    },
                  ),
                ),
                const Icon(Icons.volume_up),
                SizedBox(
                  width: 40,
                  child: Text("${(_volume * 100).round()}%",
                      textAlign: TextAlign.center),
                ),
              ],
            ),
          ] else
            const Text("No active playback."),
        ],
      ),
    );
  }

  Widget _demoButton(VoidCallback onPressed, IconData icon, String label) {
    return ElevatedButton.icon(
      onPressed: onPressed,
      icon: Icon(icon, size: 18),
      label: Text(label),
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      ),
    );
  }

  Widget _buildCustomCommandSection(
      TextEditingController controller, VoidCallback onRun, String hint) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text("Custom Command:",
            style: TextStyle(fontWeight: FontWeight.bold)),
        const SizedBox(height: 8),
        Row(
          children: [
            Expanded(
              child: TextField(
                controller: controller,
                decoration: InputDecoration(
                  hintText: hint,
                  isDense: true,
                  border: const OutlineInputBorder(),
                ),
                style: const TextStyle(fontSize: 14),
              ),
            ),
            const SizedBox(width: 8),
            _demoButton(onRun, Icons.play_arrow, "Run"),
          ],
        ),
      ],
    );
  }
}
13
likes
0
points
296
downloads

Documentation

Documentation

Publisher

verified publisherfryingpan.games

Weekly Downloads

A comprehensive Flutter plugin for executing FFmpeg, FFprobe, and FFplay commands using FFmpeg 8.0 API. Supports Android, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#ffmpeg #media-processing #transcoding #media-tagging #ffmpeg-kit

License

unknown (license)

Dependencies

ffi, flutter, logging, path, yaml

More

Packages that depend on ffmpeg_kit_extended_flutter

Packages that implement ffmpeg_kit_extended_flutter