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

如何在Flutter中使用video_player播放Firebase Storage中的视频

问题描述

我目前使用VideoPlayerController.network播放网络视频,功能正常。现在希望进一步实现:通过StreamBuilder从Firebase Firestore的快照中获取视频文件名(如8296971978270266602.mp4),调用storage.ref('videos/$videoFile').getDownloadURL()获取视频下载链接,将该链接传入video_player组件进行播放,并在ListView中展示所有视频。现有代码仅能手动传入链接播放单视频,请问如何实现上述需求?

现有代码

final FirebaseStorage storage = FirebaseStorage.instance;
late Future<void> initializeVideoPlayerFuture;
VideoPlayerController? _videoPlayerController;

@override
void initState() {
  super.initState();
  _videoPlayerController = VideoPlayerController.network(
    'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
  );
  initializeVideoPlayerFuture = _videoPlayerController!.initialize();
}

@override
void dispose() {
  _videoPlayerController!.dispose();

  super.dispose();
}

// My StreamBuilder to get snapShot from FB fireStore
StreamBuilder(
  stream: FirebaseFirestore.instance
      .collection("groups")
      .doc(groupId)
      .snapshots(),
  builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {                
    var videoFile = snapshot.data?["videoFile"];

    // ListView that I want to display all the video
    ListView.builder(
      itemCount: videoFile.length,
      shrinkWrap: true,
      itemBuilder: (context, index) {
        
        return FutureBuilder(
          future: initializeVideoPlayerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return AspectRatio(
                aspectRatio: _videoPlayerController!.value.aspectRatio,
                child: VideoPlayer(_videoPlayerController!),
              );
            } else {
              return const Center(
                child: CircularProgressIndicator(),
              );
            }
          },
        );
      },
    );
  },
);

// Download the videoFile from Fb storage(Get the url and play it in the video_player)
Future<String> downloadVideoURL(String videoFile) async {
  try {
    String downloadURL =
        await storage.ref('videos/$videoFile').getDownloadURL();
    setState(() {
      videoUrl = downloadURL;
    });
    // ignore: avoid_print
    print(downloadURL);
    return downloadURL;
  } on FirebaseException catch (e) {
    // ignore: avoid_print
    print(e);
  }
  return downloadVideoURL(videoFile);
}

解决方案

核心优化点

  1. 为每个视频创建独立的VideoPlayerController,避免共用控制器导致的播放混乱
  2. 调整Widget嵌套逻辑,确保Firestore数据更新时同步刷新视频列表
  3. 在列表项中异步获取Storage下载链接,结合控制器初始化流程
  4. 正确处理每个控制器的资源释放,避免内存泄漏

完整实现代码

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:video_player/video_player.dart';

class VideoListScreen extends StatefulWidget {
  final String groupId;

  const VideoListScreen({super.key, required this.groupId});

  @override
  State<VideoListScreen> createState() => _VideoListScreenState();
}

class _VideoListScreenState extends State<VideoListScreen> {
  final FirebaseStorage _storage = FirebaseStorage.instance;
  // 存储所有视频控制器,用于页面销毁时统一释放资源
  final List<VideoPlayerController?> _controllers = [];

  @override
  void dispose() {
    // 遍历释放所有控制器
    for (var controller in _controllers) {
      controller?.dispose();
    }
    super.dispose();
  }

  // 获取单个视频的Storage下载链接
  Future<String> _getVideoUrl(String fileName) async {
    try {
      return await _storage.ref('videos/$fileName').getDownloadURL();
    } on FirebaseException catch (e) {
      debugPrint('获取视频链接失败: ${e.message}');
      throw e;
    }
  }

  // 构建单个视频播放项
  Widget _buildVideoItem(String fileName) {
    return FutureBuilder<String>(
      future: _getVideoUrl(fileName),
      builder: (context, urlSnapshot) {
        if (urlSnapshot.connectionState == ConnectionState.waiting) {
          return const Padding(
            padding: EdgeInsets.all(16.0),
            child: Center(child: CircularProgressIndicator()),
          );
        }
        if (urlSnapshot.hasError || !urlSnapshot.hasData) {
          return const Padding(
            padding: EdgeInsets.all(16.0),
            child: Text('视频加载失败'),
          );
        }

        // 为当前视频创建独立控制器
        final controller = VideoPlayerController.network(urlSnapshot.data!);
        _controllers.add(controller);

        return FutureBuilder<void>(
          future: controller.initialize(),
          builder: (context, initSnapshot) {
            if (initSnapshot.connectionState == ConnectionState.waiting) {
              return const Padding(
                padding: EdgeInsets.all(16.0),
                child: Center(child: CircularProgressIndicator()),
              );
            }
            return Column(
              children: [
                AspectRatio(
                  aspectRatio: controller.value.aspectRatio,
                  child: VideoPlayer(controller),
                ),
                // 添加进度条和播放控制按钮
                VideoProgressIndicator(controller, allowScrubbing: true),
                IconButton(
                  onPressed: () {
                    setState(() {
                      controller.value.isPlaying
                          ? controller.pause()
                          : controller.play();
                    });
                  },
                  icon: Icon(
                    controller.value.isPlaying
                        ? Icons.pause
                        : Icons.play_arrow,
                  ),
                ),
                const SizedBox(height: 16),
              ],
            );
          },
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('视频列表')),
      body: StreamBuilder<DocumentSnapshot>(
        stream: FirebaseFirestore.instance
            .collection('groups')
            .doc(widget.groupId)
            .snapshots(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (!snapshot.hasData || snapshot.data == null) {
            return const Center(child: Text('无视频数据'));
          }

          // 从Firestore文档中取出视频文件名列表(确保videoFile是数组类型字段)
          final videoFiles = snapshot.data!['videoFile'] as List?;
          if (videoFiles == null || videoFiles.isEmpty) {
            return const Center(child: Text('暂无视频'));
          }

          return ListView.builder(
            padding: const EdgeInsets.all(8),
            itemCount: videoFiles.length,
            itemBuilder: (context, index) {
              final fileName = videoFiles[index] as String;
              return _buildVideoItem(fileName);
            },
          );
        },
      ),
    );
  }
}

关键说明

  • 控制器管理:用列表统一存储所有视频控制器,页面销毁时批量释放,避免内存泄漏
  • 异步流程:分两步用FutureBuilder处理「获取下载链接」和「初始化控制器」的异步状态,加载过程显示进度提示
  • 错误处理:针对链接获取失败、控制器初始化失败的场景添加友好提示
  • 交互优化:增加进度条和播放/暂停按钮,提升视频播放的交互体验

内容的提问来源于stack exchange,提问作者It'sPhil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 16:15:41