ffmpeg_streamer 0.2.0 copy "ffmpeg_streamer: ^0.2.0" to clipboard
ffmpeg_streamer: ^0.2.0 copied to clipboard

A cross-platform Flutter FFI plugin that embeds FFmpeg to decode audio & video and stream raw frames — works on Android, iOS, macOS, Windows & Linux.

example/lib/main.dart

import 'dart:async';
import 'dart:ui' as ui;
import 'package:ffmpeg_streamer/ffmpeg_streamer.dart';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';

void main() {
  runApp(const MaterialApp(home: MyApp()));
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  FfmpegDecoder? _decoder;
  MediaInfo? _mediaInfo;
  ui.Image? _currentFrame;
  int _currentFrameIndex = 0;

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

  Future<void> _pickFile() async {
    FilePickerResult? result = await FilePicker.platform.pickFiles();

    if (result != null && result.files.single.path != null) {
      await _openMedia(result.files.single.path!);
    }
  }

  Future<void> _openMedia(String path) async {
    // Cleanup previous
    await _decoder?.release();
    setState(() {
      _mediaInfo = null;
      _currentFrame = null;
      _currentFrameIndex = 0;
    });

    try {
      final decoder = FfmpegDecoder();
      final success = await decoder.openMedia(path);

      if (!success) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Failed to open media file')),
        );
        return;
      }

      // Get the first frame (contains both video and audio)
      final mediaFrame = await decoder.getFrameAtIndex(0);

      if (mediaFrame != null && mediaFrame.video != null) {
        _renderFrame(mediaFrame.video!);

        setState(() {
          _decoder = decoder;
          _mediaInfo = _createMediaInfo(decoder);
        });
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('No video data found in media')),
        );
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error opening media: $e')),
      );
    }
  }

  MediaInfo _createMediaInfo(FfmpegDecoder decoder) {
    return MediaInfo(
      width: decoder.videoWidth,
      height: decoder.videoHeight,
      fps: decoder.fps,
      duration: Duration(milliseconds: decoder.durationMs),
      totalFrames: decoder.totalFrames,
      audioSampleRate: decoder.audioSampleRate,
      audioChannels: decoder.audioChannels,
    );
  }

  Future<void> _renderFrame(VideoFrame frame) async {
    final completer = Completer<ui.Image>();
    ui.decodeImageFromPixels(
      frame.rgbaBytes,
      frame.width,
      frame.height,
      ui.PixelFormat.rgba8888,
      (image) {
        completer.complete(image);
      },
    );
    final image = await completer.future;

    if (mounted) {
      setState(() {
        _currentFrame = image;
        _currentFrameIndex = frame.frameId;
      });
    }
  }

  Future<void> _goToFrame(int frameIndex) async {
    if (_decoder == null) return;

    final mediaFrame = await _decoder!.getFrameAtIndex(frameIndex);

    if (mediaFrame != null && mediaFrame.video != null) {
      _renderFrame(mediaFrame.video!);
    }
  }

  Future<void> _goToPreviousFrame() async {
    if (_decoder == null) return;

    final mediaFrame = await _decoder!.previousFrame();

    if (mediaFrame != null && mediaFrame.video != null) {
      _renderFrame(mediaFrame.video!);
    }
  }

  Future<void> _goToNextFrame() async {
    if (_decoder == null) return;

    final mediaFrame = await _decoder!.nextFrame();

    if (mediaFrame != null && mediaFrame.video != null) {
      _renderFrame(mediaFrame.video!);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('FFmpeg Streamer Example')),
      body: Column(
        children: [
          if (_mediaInfo != null)
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  Text(
                      '${_mediaInfo!.width}x${_mediaInfo!.height} @ ${_mediaInfo!.fps.toStringAsFixed(2)} fps'),
                  Text(
                      'Duration: ${_mediaInfo!.duration} | Total frames: ${_mediaInfo!.totalFrames}'),
                  Text('Current frame: $_currentFrameIndex'),
                ],
              ),
            ),
          Expanded(
            child: Center(
              child: _currentFrame != null
                  ? AspectRatio(
                      aspectRatio: _mediaInfo!.width / _mediaInfo!.height,
                      child: CustomPaint(
                        painter: VideoPainter(_currentFrame!),
                      ),
                    )
                  : const Text('No video loaded - Pick a file to start'),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                // Frame index input
                if (_mediaInfo != null)
                  Padding(
                    padding: const EdgeInsets.only(bottom: 8.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: [
                        SizedBox(
                          width: 100,
                          child: TextField(
                            keyboardType: TextInputType.number,
                            decoration: InputDecoration(
                              labelText: 'Go to frame',
                              border: const OutlineInputBorder(),
                              hintText: '0-${_mediaInfo!.totalFrames - 1}',
                            ),
                            onSubmitted: (value) {
                              final frameIndex = int.tryParse(value);
                              if (frameIndex != null) {
                                _goToFrame(frameIndex);
                              }
                            },
                          ),
                        ),
                        ElevatedButton(
                          onPressed: () async {
                            // Go to random frame
                            final randomIndex =
                                (DateTime.now().millisecondsSinceEpoch %
                                        _mediaInfo!.totalFrames)
                                    .toInt();
                            await _goToFrame(randomIndex);
                          },
                          child: const Text('Random Frame'),
                        ),
                      ],
                    ),
                  ),
                // Navigation buttons
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: _pickFile,
                      child: const Text('Pick File'),
                    ),
                    if (_decoder != null) ...[
                      ElevatedButton(
                        onPressed: _goToPreviousFrame,
                        child: const Text('Previous Frame'),
                      ),
                      ElevatedButton(
                        onPressed: _goToNextFrame,
                        child: const Text('Next Frame'),
                      ),
                    ]
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class VideoPainter extends CustomPainter {
  final ui.Image image;

  VideoPainter(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawImageRect(
      image,
      Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
      Rect.fromLTWH(0, 0, size.width, size.height),
      Paint()..filterQuality = FilterQuality.low,
    );
  }

  @override
  bool shouldRepaint(covariant VideoPainter oldDelegate) {
    return image != oldDelegate.image;
  }
}
3
likes
0
points
274
downloads

Publisher

unverified uploader

Weekly Downloads

A cross-platform Flutter FFI plugin that embeds FFmpeg to decode audio & video and stream raw frames — works on Android, iOS, macOS, Windows & Linux.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on ffmpeg_streamer

Packages that implement ffmpeg_streamer