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

Flutter plugin for extracting video thumbnails on Android and iOS.

example/lib/main.dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:snapframes/snapframes.dart';

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

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

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

class _MyAppState extends State<MyApp> {
  String? _videoPath;
  List<Uint8List>? _frames;
  String? _error;
  bool _loading = false;

  // ---------- UI actions ----------

  Future<void> _pickVideo() async {
    setState(() {
      _frames = null;
      _error = null;
    });
    try {
      final result = await FilePicker.platform.pickFiles(
        type: FileType.video,
        allowMultiple: false,
      );
      final path = result?.files.single.path;
      if (path != null) {
        setState(() => _videoPath = path);
      }
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }

  Future<void> _grab10Frames() async {
    if (_videoPath == null) {
      setState(() => _error = 'Сначала выберите видео');
      return;
    }
    setState(() {
      _loading = true;
      _error = null;
      _frames = null;
    });

    try {
      final frames = await grab10FramesSafely(
        source: _videoPath!,
        format: SnapImageFormat.jpg,
        quality: 80,
        fastKeyframesFirst: true, // сначала быстрый режим по ключевым кадрам
      );
      setState(() => _frames = frames);
    } catch (e) {
      setState(() => _error = e.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  // ---------- UI ----------

  @override
  Widget build(BuildContext context) {
    final theme = ThemeData(useMaterial3: true, colorSchemeSeed: Colors.green);
    return MaterialApp(
      theme: theme,
      home: Scaffold(
        appBar: AppBar(title: const Text('snapframes demo')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: ListView(
            children: [
              Row(
                children: [
                  Expanded(
                    child: Text(
                      _videoPath ?? 'Видео не выбрано',
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                  const SizedBox(width: 12),
                  FilledButton.icon(
                    onPressed: _loading ? null : _pickVideo,
                    icon: const Icon(Icons.video_file_outlined),
                    label: const Text('Pick video'),
                  ),
                ],
              ),
              const SizedBox(height: 12),
              FilledButton.icon(
                onPressed: _loading ? null : _grab10Frames,
                icon: _loading
                    ? const SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                )
                    : const Icon(Icons.collections),
                label: const Text('Grab 10 frames (safe)'),
              ),
              const SizedBox(height: 16),
              if (_error != null)
                Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
              if (_frames != null) ...[
                Text('Получено кадров: ${_frames!.length}'),
                const SizedBox(height: 8),
                GridView.builder(
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    crossAxisSpacing: 8,
                    mainAxisSpacing: 8,
                  ),
                  itemCount: _frames!.length,
                  itemBuilder: (context, index) {
                    return ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Image.memory(_frames![index], fit: BoxFit.cover),
                    );
                  },
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

/// ---------------- SAFETY HELPERS ----------------

int _safetyMarginMsFor(Duration d) {
  final total = d.inMilliseconds;
  if (total <= 0) return 500;
  // 1 сек, но не больше четверти длительности и не меньше 250 мс
  final margin = total ~/ 4;
  return margin.clamp(250, 1000);
}

/// Равномерные таймстампы внутри [0, duration - safety).
List<int> _safeEvenTimestamps(Duration duration, {int count = 10}) {
  final total = duration.inMilliseconds;
  final safety = _safetyMarginMsFor(duration);
  final maxTs = (total - safety).clamp(0, total - 1);
  if (maxTs <= 0 || count <= 0) return const [];
  final step = (maxTs ~/ (count + 1)).clamp(1, maxTs);
  return List.generate(count, (i) => ((i + 1) * step).clamp(0, maxTs));
}

/// Кандидаты вокруг точки: сам ts и небольшие сдвиги.
List<int> _candidatesAround(int ts, int maxTs) {
  final raw = <int>{ts, ts - 120, ts + 120, ts - 240, ts + 240};
  return raw.where((v) => v >= 0 && v <= maxTs).toList();
}

/// Получить 10 кадров безопасно.
/// - Если [fastKeyframesFirst] = true — сперва батч по ключевым кадрам (очень быстро и устойчиво).
/// - Недостающие кадры добираются точными одиночными запросами со сдвигами.
Future<List<Uint8List>> grab10FramesSafely({
  required String source,
  int? width,
  int? height,
  SnapImageFormat format = SnapImageFormat.jpg,
  int quality = 85,
  bool fastKeyframesFirst = true,
}) async {
  final duration = await getDuration(source);
  final total = duration.inMilliseconds;
  final safety = _safetyMarginMsFor(duration);
  final maxTs = (total - safety).clamp(0, total - 1);

  final timestamps = _safeEvenTimestamps(duration, count: 10);
  if (timestamps.isEmpty) return const [];

  final frames = <Uint8List>[];

  // Попытка №1: быстрый батч по ключевым кадрам
  if (fastKeyframesFirst) {
    try {
      final fastReq = BatchRequest(
        source: source,
        timestampsMs: timestamps,
        width: width,
        height: height,
        format: format,
        quality: quality,
        keyframeOnly: true, // ВАЖНО: быстрый режим
      );
      final fastFrames = await getFramesBytes(fastReq);
      frames.addAll(fastFrames);
    } catch (_) {
      // ок, перейдём к поштучным
    }
  }

  // Если чего-то не добрали — берём поштучно с точным seek и сдвигами
  if (frames.length < timestamps.length) {
    // определим, сколько ещё нужно
    final need = timestamps.length - frames.length;

    // собираем поштучно (точный режим), идём по исходным ts
    for (final ts in timestamps) {
      if (frames.length >= timestamps.length) break; // уже добрали
      Uint8List? got;

      // кандидаты вокруг ts
      for (final c in _candidatesAround(ts, maxTs)) {
        try {
          got = await getFrameBytes(
            SnapRequest(
              source: source,
              timestampMs: c,
              width: width,
              height: height,
              format: format,
              quality: quality,
              keyframeOnly: true,
              // можно добавить keyframeOnly в SnapRequest, если нужно
            ),
          );
          break;
        } catch (_) {/* пробуем следующий кандидат */}
      }
      if (got != null) {
        frames.add(got);
      }
      if (frames.length >= timestamps.length) break;
      if (frames.length >= timestamps.length - need) continue;
    }
  }

  return frames;
}
0
likes
130
points
13
downloads

Publisher

verified publisherkotelnikoff.dev

Weekly Downloads

Flutter plugin for extracting video thumbnails on Android and iOS.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on snapframes

Packages that implement snapframes