You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中视频缩略图渲染速度过慢的优化求助

本地视频缩略图加载缓慢问题

我用Flutter的VideoPlayer实现视频缩略图展示,代码如下:

class VideoPlayerWidget extends Stateful Widget {
  VideoPlayerWidget({
    Key? key,
    required this.path,
    required this.sourceType,
    this.height,
    this.width,
  }) : super(key: key);

  final String path;
  final SourceType sourceType;
  final double? height;
  final double? width;

  @override
  State<VideoPlayerWidget> createState() => _VideoPlayerWidgetState();
}

class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
  late VideoPlayerController _videoPlayerController;

  @override
  void initState() {
    switch (widget.sourceType) {
      case SourceType.asset:
        _videoPlayerController = VideoPlayerController.asset(widget.path);
        break;
      case SourceType.network:
        _videoPlayerController = VideoPlayerController.network(widget.path);
        break;
      case SourceType.file:
        _videoPlayerController = VideoPlayerController.file(File(widget.path));
        break;
      case SourceType.contentUri:
        _videoPlayerController =
            VideoPlayerController.contentUri(Uri.parse(widget.path));
        break;
    }

    _videoPlayerController.initialize().then((value) => setState((){}));

    super.initState();
  }

  @override
  void dispose() {
    _videoPlayerController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if ( !_videoPlayerController.value.isInitialized) {
      return Skeleton(
        height: widget.height ?? 0,
        width: widget.width ?? 0,
      );
    }
    return GestureDetector(
      onTap: () {
        ChewiePlayingScreen(videoPlayerController: _videoPlayerController,).launch(context);
      },
      child: Stack(
        children: [
          SizedBox(
            height: widget.height,
            width: widget.width,
            child: _videoPlayerController.value.isInitialized
                ? VideoPlayer(_videoPlayerController)
                : Offstage(),
          ),
          Positioned.fill(
            child: Align(
              alignment: Alignment.center,
              child: Icon(
                Icons.play_circle_outline,
                color: SVAppWhite,
                size: 35,
              ),
            ),
          )
        ],
      ),
    );
  }
}

class ChewiePlayingScreen extends Stateful Widget {
  const ChewiePlayingScreen({Key? key, required this.videoPlayerController}) : super(key: key);

  final VideoPlayerController videoPlayerController;

  @override
  State<ChewiePlayingScreen> createState() => _ChewiePlayingScreenState();
}

class _ChewiePlayingScreenState extends State<ChewiePlayingScreen> {
  late ChewieController chewieController;

  @override
  void initState() {
    chewieController = ChewieController(
      videoPlayerController: widget.videoPlayerController,
      aspectRatio: widget.videoPlayerController.value.aspectRatio,
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AspectRatio(
        aspectRatio: widget.videoPlayerController.value.aspectRatio,
        child: Chewie(
          controller: chewieController,
        ),
      ),
    );
  }
}

相机录制的视频缩略图加载正常,但通过ImagePicker获取的本地3MB视频,缩略图加载耗时长达20秒。


解决方案

1. 使用专门的缩略图提取包(推荐)

VideoPlayerController初始化会加载视频的完整元数据甚至部分流,对于非系统相机录制的视频(比如ImagePicker处理过的)效率极低。改用thumbnail包直接提取视频第一帧,速度提升明显。

步骤:

  • 添加依赖到pubspec.yaml:
dependencies:
  thumbnail: ^1.0.0
  • 修改_VideoPlayerWidgetState,替换原有的VideoPlayerController逻辑:
class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
  Uint8List? _thumbnailBytes;
  bool _isLoading = true;

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

  Future<void> _generateThumbnail() async {
    try {
      Uint8List bytes;
      switch (widget.sourceType) {
        case SourceType.asset:
          final assetPath = await DefaultAssetBundle.of(context).load(widget.path);
          bytes = await Thumbnail.image(
            image: assetPath.buffer.asUint8List(),
            height: widget.height?.toInt() ?? 200,
            width: widget.width?.toInt() ?? 200,
          );
          break;
        case SourceType.file:
          bytes = await Thumbnail.file(
            file: File(widget.path),
            height: widget.height?.toInt() ?? 200,
            width: widget.width?.toInt() ?? 200,
          );
          break;
        case SourceType.network:
          bytes = await Thumbnail.network(
            widget.path,
            height: widget.height?.toInt() ?? 200,
            width: widget.width?.toInt() ?? 200,
          );
          break;
        case SourceType.contentUri:
          final file = await _contentUriToFile(Uri.parse(widget.path));
          bytes = await Thumbnail.file(
            file: file,
            height: widget.height?.toInt() ?? 200,
            width: widget.width?.toInt() ?? 200,
          );
          break;
      }
      setState(() {
        _thumbnailBytes = bytes;
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
    }
  }

  // 辅助方法:将contentUri转为File
  Future<File> _contentUriToFile(Uri uri) async {
    final file = File('${(await getTemporaryDirectory()).path}/${DateTime.now().millisecondsSinceEpoch}.tmp');
    final inputStream = await context.contentResolver.openInputStream(uri);
    final outputStream = file.openWrite();
    await inputStream?.pipe(outputStream);
    await inputStream?.close();
    await outputStream.close();
    return file;
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Skeleton(
        height: widget.height ?? 0,
        width: widget.width ?? 0,
      );
    }
    return GestureDetector(
      onTap: () {
        // 点击后再初始化VideoPlayerController进入播放页面
        late VideoPlayerController controller;
        switch (widget.sourceType) {
          case SourceType.asset:
            controller = VideoPlayerController.asset(widget.path);
            break;
          case SourceType.network:
            controller = VideoPlayerController.network(widget.path);
            break;
          case SourceType.file:
            controller = VideoPlayerController.file(File(widget.path));
            break;
          case SourceType.contentUri:
            controller = VideoPlayerController.contentUri(Uri.parse(widget.path));
            break;
        }
        controller.initialize().then((_) {
          ChewiePlayingScreen(videoPlayerController: controller).launch(context);
        });
      },
      child: Stack(
        children: [
          if (_thumbnailBytes != null)
            SizedBox(
              height: widget.height,
              width: widget.width,
              child: Image.memory(
                _thumbnailBytes!,
                fit: BoxFit.cover,
              ),
            ),
          Positioned.fill(
            child: Align(
              alignment: Alignment.center,
              child: Icon(
                Icons.play_circle_outline,
                color: SVAppWhite,
                size: 35,
              ),
            ),
          )
        ],
      ),
    );
  }
}

2. 优化VideoPlayerController初始化(备选)

如果不想引入第三方包,可以尝试限制VideoPlayerController只加载元数据,避免预加载视频流:

_videoPlayerController = VideoPlayerController.file(File(widget.path))
  ..setLooping(false)
  ..setVolume(0)
  ..initialize().then((_) {
    setState(() {});
    // 只需要第一帧,初始化后立即暂停
    _videoPlayerController.pause();
  });

但这种方法对ImagePicker获取的视频提升有限,不如专门的缩略图包效果显著。


内容的提问来源于stack exchange,提问作者mbakabilal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 18:54:56