Flutter视频缩略图生成插件get_video_thumbnail的使用

Flutter视频缩略图生成插件get_video_thumbnail的使用

插件简介

get_video_thumbnail 是一个基于 get_thumbnail_video 的Flutter插件,修复了原插件中的一些内存泄漏问题。该插件可以从视频文件或URL生成缩略图,并支持将图像保存到内存或文件中。它提供了丰富的选项来控制图像格式、分辨率和质量,支持iOS、Android和Web平台。

版本信息

  • 版本: 0.6.1

功能方法

方法名 参数 描述 返回值
thumbnailData String video, Map<String, dynamic>? headers, ImageFormat imageFormat, int maxHeight, int maxWidth, int timeMs, int quality 从视频生成缩略图并返回图像数据 Future<Uint8List>
thumbnailFile String video, Map<String, dynamic>? headers, String? thumbnailPath, ImageFormat imageFormat, int maxHeight, int maxWidth, int timeMs, int quality 从视频生成缩略图并保存为文件 Future<String>

注意事项:

  • 如果同时设置了 maxHeightmaxWidth,在Android平台上会直接缩放到指定的高度和宽度。
  • 生成网络资源的缩略图时,video 必须是正确编码的URL。

使用示例

1. 安装依赖

pubspec.yaml 文件中添加 get_video_thumbnail 依赖:

dependencies:
  get_video_thumbnail: ^0.6.4
2. 导入包

在 Dart 文件中导入 get_video_thumbnail 包:

import 'package:get_video_thumbnail/get_video_thumbnail.dart';
3. 从本地视频文件生成缩略图(内存中)

以下代码展示了如何从本地视频文件生成缩略图并将其保存在内存中:

final uint8list = await VideoThumbnail.thumbnailData(
  video: videofile.path, // 视频文件路径
  imageFormat: ImageFormat.JPEG, // 图像格式
  maxWidth: 128, // 指定缩略图的宽度,高度会根据源视频的宽高比自动调整
  quality: 25, // 图像质量 (0-100)
);
4. 从网络视频URL生成缩略图(保存为文件)

以下代码展示了如何从网络视频URL生成缩略图并将其保存为文件:

XFile thumbnailFile = await VideoThumbnail.thumbnailFile(
  video: "https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4", // 视频URL
  thumbnailPath: (await getTemporaryDirectory()).path, // 缩略图保存路径
  imageFormat: ImageFormat.WEBP, // 图像格式
  maxHeight: 64, // 指定缩略图的高度,宽度会根据源视频的宽高比自动调整
  quality: 75, // 图像质量 (0-100)
);

final image = kIsWeb ? Image.network(thumbnailFile.path) : Image.file(File(thumbnailFile.path));
5. 从 pubspec.yaml 中声明的视频资源生成缩略图

以下代码展示了如何从 pubspec.yaml 中声明的视频资源生成缩略图:

final byteData = await rootBundle.load("assets/my_video.mp4"); // 加载视频资源
Directory tempDir = await getTemporaryDirectory(); // 获取临时目录

File tempVideo = File("${tempDir.path}/assets/my_video.mp4")
  ..createSync(recursive: true)
  ..writeAsBytesSync(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); // 将视频资源写入临时文件

final fileName = await VideoThumbnail.thumbnailFile(
  video: tempVideo.path, // 临时视频文件路径
  thumbnailPath: (await getTemporaryDirectory()).path, // 缩略图保存路径
  imageFormat: ImageFormat.PNG, // 图像格式
  quality: 100, // 图像质量 (0-100)
);

Web平台的限制

在Web平台上使用 get_video_thumbnail 时,有一些限制需要注意:

  1. CORS 头部:服务器必须在响应中包含适当的 CORS 头部,如 Access-Control-Allow-OriginAccess-Control-Allow-Methods
  2. HTTP Range 头部:服务器必须支持 HTTP Range 请求。如果服务器不支持范围请求,插件可能会从视频的第一帧生成缩略图,而不是指定的时间点。

完整示例Demo

以下是一个完整的示例应用,展示了如何使用 get_video_thumbnail 插件从视频生成缩略图,并在界面上显示生成的缩略图。

import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get_video_thumbnail/get_video_thumbnail.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';

void main() => runApp(const MyApp());

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: DemoHome(),
    );
  }
}

class ThumbnailRequest {
  const ThumbnailRequest({
    required this.video,
    required this.thumbnailPath,
    required this.imageFormat,
    required this.maxHeight,
    required this.maxWidth,
    required this.timeMs,
    required this.quality,
    required this.attachHeaders,
  });

  final String video;
  final String? thumbnailPath;
  final ImageFormat imageFormat;
  final int maxHeight;
  final int maxWidth;
  final int timeMs;
  final int quality;
  final bool attachHeaders;
}

class ThumbnailResult {
  const ThumbnailResult({
    required this.image,
    required this.dataSize,
    required this.height,
    required this.width,
  });

  final Image image;
  final int dataSize;
  final int height;
  final int width;
}

Future<ThumbnailResult> genThumbnail(ThumbnailRequest r) async {
  Uint8List bytes;
  final completer = Completer<ThumbnailResult>();
  
  if (r.thumbnailPath != null) {
    final thumbnailFile = await VideoThumbnail.thumbnailFile(
      video: r.video,
      headers: r.attachHeaders
          ? const {
              'USERHEADER1': 'user defined header1',
              'USERHEADER2': 'user defined header2',
            }
          : null,
      thumbnailPath: r.thumbnailPath,
      imageFormat: r.imageFormat,
      maxHeight: r.maxHeight,
      maxWidth: r.maxWidth,
      timeMs: r.timeMs,
      quality: r.quality,
    );

    debugPrint('thumbnail file is located: $thumbnailFile');
    bytes = await thumbnailFile.readAsBytes();
  } else {
    bytes = await VideoThumbnail.thumbnailData(
      video: r.video,
      headers: r.attachHeaders
          ? const {
              'USERHEADER1': 'user defined header1',
              'USERHEADER2': 'user defined header2',
            }
          : null,
      imageFormat: r.imageFormat,
      maxHeight: r.maxHeight,
      maxWidth: r.maxWidth,
      timeMs: r.timeMs,
      quality: r.quality,
    );
  }

  final imageDataSize = bytes.length;
  debugPrint('image size: $imageDataSize');

  final image = Image.memory(bytes);
  image.image.resolve(ImageConfiguration.empty).addListener(
    ImageStreamListener(
      (ImageInfo info, bool _) {
        completer.complete(
          ThumbnailResult(
            image: image,
            dataSize: imageDataSize,
            height: info.image.height,
            width: info.image.width,
          ),
        );
      },
      onError: completer.completeError,
    ),
  );
  return completer.future;
}

class GenThumbnailImage extends StatefulWidget {
  const GenThumbnailImage({
    super.key,
    required this.thumbnailRequest,
  });
  final ThumbnailRequest thumbnailRequest;

  [@override](/user/override)
  State<GenThumbnailImage> createState() => _GenThumbnailImageState();
}

class _GenThumbnailImageState extends State<GenThumbnailImage> {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return FutureBuilder<ThumbnailResult>(
      future: genThumbnail(widget.thumbnailRequest),
      builder: (BuildContext context, AsyncSnapshot<ThumbnailResult> snapshot) {
        if (snapshot.hasData) {
          final data = snapshot.data!;
          final image = data.image;
          final width = data.width;
          final height = data.height;
          final dataSize = data.dataSize;
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Center(
                child: Text(
                  "Image ${widget.thumbnailRequest.thumbnailPath == null ? 'data size' : 'file size'}: $dataSize, width:$width, height:$height",
                ),
              ),
              Container(color: Colors.grey),
              image,
            ],
          );
        } else if (snapshot.hasError) {
          return Container(
            padding: const EdgeInsets.all(8),
            color: Colors.red,
            child: Text('Error:\n${snapshot.error}\n\n${snapshot.stackTrace}'),
          );
        } else {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Generating the thumbnail for: ${widget.thumbnailRequest.video}...',
              ),
              const SizedBox(height: 10),
              const CircularProgressIndicator(),
            ],
          );
        }
      },
    );
  }
}

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

  [@override](/user/override)
  State<DemoHome> createState() => _DemoHomeState();
}

class _DemoHomeState extends State<DemoHome> {
  final _editNode = FocusNode();
  final _video = TextEditingController(
    text: 'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
  );
  ImageFormat _format = ImageFormat.JPEG;
  int _quality = 50;
  bool _attachHeaders = false;
  int _sizeH = 0;
  int _sizeW = 0;
  int _timeMs = 0;

  GenThumbnailImage? _futureImage;

  String? _tempDir;

  [@override](/user/override)
  void initState() {
    super.initState();

    if (!kIsWeb) {
      getTemporaryDirectory().then((d) => _tempDir = d.path);
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    final settings = <Widget>[
      Slider(
        value: _sizeH * 1.0,
        onChanged: (v) => setState(() {
          _editNode.unfocus();
          _sizeH = v.toInt();
        }),
        max: 256,
        divisions: 256,
        label: '$_sizeH',
      ),
      Center(
        child: (_sizeH == 0)
            ? const Text(
                "Original of the video's height or scaled by the source aspect ratio",
              )
            : Text('Max height: $_sizeH(px)'),
      ),
      Slider(
        value: _sizeW * 1.0,
        onChanged: (v) => setState(() {
          _editNode.unfocus();
          _sizeW = v.toInt();
        }),
        max: 256,
        divisions: 256,
        label: '$_sizeW',
      ),
      Center(
        child: (_sizeW == 0)
            ? const Text(
                "Original of the video's width or scaled by source aspect ratio",
              )
            : Text('Max width: $_sizeW(px)'),
      ),
      Slider(
        value: _timeMs * 1.0,
        onChanged: (v) => setState(() {
          _editNode.unfocus();
          _timeMs = v.toInt();
        }),
        max: 10.0 * 1000,
        divisions: 1000,
        label: '$_timeMs',
      ),
      Center(
        child: (_timeMs == 0)
            ? const Text('The beginning of the video')
            : Text('The closest frame at $_timeMs(ms) of the video'),
      ),
      Slider(
        value: _quality * 1.0,
        onChanged: (v) => setState(() {
          _editNode.unfocus();
          _quality = v.toInt();
        }),
        max: 100,
        divisions: 100,
        label: '$_quality',
      ),
      Center(child: Text('Quality: $_quality')),
      SwitchListTile(
        title: const Text('Attach Headers'),
        value: _attachHeaders,
        onChanged: (value) => setState(() => _attachHeaders = value),
        secondary: const Icon(Icons.http),
      ),
      Padding(
        padding: const EdgeInsets.fromLTRB(2, 10, 2, 8),
        child: InputDecorator(
          decoration: const InputDecoration(
            border: OutlineInputBorder(),
            filled: true,
            isDense: true,
            labelText: 'Thumbnail Format',
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Radio<ImageFormat>(
                    groupValue: _format,
                    value: ImageFormat.JPEG,
                    onChanged: (v) => setState(() {
                      _format = v!;
                      _editNode.unfocus();
                    }),
                  ),
                  const Text('JPEG'),
                ],
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Radio<ImageFormat>(
                    groupValue: _format,
                    value: ImageFormat.PNG,
                    onChanged: (v) => setState(() {
                      _format = v!;
                      _editNode.unfocus();
                    }),
                  ),
                  const Text('PNG'),
                ],
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Radio<ImageFormat>(
                    groupValue: _format,
                    value: ImageFormat.WEBP,
                    onChanged: (v) => setState(() {
                      _format = v!;
                      _editNode.unfocus();
                    }),
                  ),
                  const Text('WebP'),
                ],
              ),
            ],
          ),
        ),
      )
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Thumbnail Plugin example'),
      ),
      body: SingleChildScrollView(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.fromLTRB(2, 10, 2, 8),
              child: TextField(
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  filled: true,
                  isDense: true,
                  labelText: 'Video URI',
                ),
                maxLines: null,
                controller: _video,
                focusNode: _editNode,
                keyboardType: TextInputType.url,
                textInputAction: TextInputAction.done,
                onEditingComplete: _editNode.unfocus,
              ),
            ),
            for (var i in settings) i,
            ConstrainedBox(
              constraints: BoxConstraints(
                maxHeight: MediaQuery.of(context).size.height * 0.4,
              ),
              child: Container(
                color: Colors.grey[300],
                child: ListView(
                  shrinkWrap: true,
                  children: <Widget>[
                    if (_futureImage != null) _futureImage! else const SizedBox(),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () async {
              final video = await ImagePicker().pickVideo(source: ImageSource.camera);
              setState(() {
                _video.text = video?.path ?? '';
              });
            },
            tooltip: 'Capture a video',
            child: const Icon(Icons.videocam),
          ),
          const SizedBox(width: 5),
          FloatingActionButton(
            onPressed: () async {
              final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
              setState(() {
                _video.text = video?.path ?? '';
              });
            },
            tooltip: 'Pick a video',
            child: const Icon(Icons.local_movies),
          ),
          const SizedBox(width: 20),
          FloatingActionButton(
            tooltip: 'Generate a data of thumbnail',
            onPressed: () async {
              setState(() {
                _futureImage = GenThumbnailImage(
                  thumbnailRequest: ThumbnailRequest(
                    video: _video.text,
                    thumbnailPath: null,
                    imageFormat: _format,
                    maxHeight: _sizeH,
                    maxWidth: _sizeW,
                    timeMs: _timeMs,
                    quality: _quality,
                    attachHeaders: _attachHeaders,
                  ),
                );
              });
            },
            child: const Text('Data'),
          ),
          const SizedBox(width: 5),
          FloatingActionButton(
            tooltip: 'Generate a file of thumbnail',
            onPressed: () async {
              setState(() {
                _futureImage = GenThumbnailImage(
                  thumbnailRequest: ThumbnailRequest(
                    video: _video.text,
                    thumbnailPath: _tempDir,
                    imageFormat: _format,
                    maxHeight: _sizeH,
                    maxWidth: _sizeW,
                    timeMs: _timeMs,
                    quality: _quality,
                    attachHeaders: _attachHeaders,
                  ),
                );
              });
            },
            child: const Text('File'),
          ),
        ],
      ),
    );
  }
}

更多关于Flutter视频缩略图生成插件get_video_thumbnail的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter视频缩略图生成插件get_video_thumbnail的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用get_video_thumbnail插件来生成视频缩略图的示例代码。

首先,确保你已经在pubspec.yaml文件中添加了get_video_thumbnail依赖:

dependencies:
  flutter:
    sdk: flutter
  get_video_thumbnail: ^0.2.4  # 请检查最新版本号并替换

然后运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤使用get_video_thumbnail插件:

  1. 导入插件

在你的Dart文件中导入插件:

import 'package:get_video_thumbnail/get_video_thumbnail.dart';
  1. 使用插件生成缩略图

下面是一个完整的示例,展示了如何使用get_video_thumbnail从视频文件中生成缩略图:

import 'package:flutter/material.dart';
import 'package:get_video_thumbnail/get_video_thumbnail.dart';
import 'dart:io';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  File? thumbnailFile;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Video Thumbnail Generator'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: _generateThumbnail,
                child: Text('Generate Thumbnail'),
              ),
              SizedBox(height: 20),
              if (thumbnailFile != null)
                Image.file(
                  thumbnailFile!,
                  width: 200,
                  height: 200,
                  fit: BoxFit.cover,
                ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _generateThumbnail() async {
    // 视频文件路径,可以是本地路径或应用内的资源路径
    String videoPath = 'path/to/your/video.mp4';  // 替换为你的视频文件路径

    // 生成缩略图
    File? thumbnail = await VideoThumbnail.thumbnailData(
      video: videoPath,
      imageFormat: ImageFormat.JPEG,  // 或者 ImageFormat.PNG
      quality: 25,  // 质量参数,范围从0到100
      timeMs: 1000,  // 生成缩略图的时间点(毫秒),默认为视频的第一帧
    );

    if (thumbnail != null) {
      setState(() {
        thumbnailFile = thumbnail;
      });
    } else {
      // 处理错误情况
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to generate thumbnail')),
      );
    }
  }
}

解释

  • 导入插件:在文件顶部导入get_video_thumbnail包。
  • UI布局:使用ScaffoldColumn布局了一个简单的界面,包含一个按钮和一个用于显示缩略图的Image组件。
  • 生成缩略图_generateThumbnail方法使用VideoThumbnail.thumbnailData方法来生成视频的缩略图。你需要提供视频文件的路径、图像格式、质量和生成缩略图的时间点(毫秒)。
  • 更新UI:生成缩略图成功后,将缩略图文件保存到thumbnailFile变量中,并使用setState方法更新UI以显示缩略图。

请确保将videoPath替换为你实际视频文件的路径。

这个示例展示了如何在Flutter应用中集成和使用get_video_thumbnail插件来生成视频缩略图。

回到顶部