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

Flutter:Chewie与VideoPlayer释放后仍后台播放问题求助

Flutter视频播放器关闭后后台持续播放的问题修复

问题根源分析

  1. 控制器重复创建且未清理:在FutureBuilder的builder回调中每次都会创建新的VideoPlayerController和ChewieController实例,但旧实例未被销毁。FutureBuilder会在Widget重建时多次执行builder,导致多个控制器同时运行,即使当前Widget的dispose被调用,之前遗弃的控制器仍在后台播放。
  2. dispose操作顺序错误:原代码中先调用控制器的dispose(),再执行pause(),此时控制器已被释放,pause操作完全无效,甚至会触发异常。
  3. Future未缓存导致重复请求:fetchJsonContent()未被缓存,每次build都会重新发起网络请求,进一步加剧了控制器重复创建的问题。

修复步骤

  1. 缓存网络请求Future:在initState中初始化并缓存视频URL的请求Future,避免重复发起请求。
  2. 控制器单一实例化:将控制器改为可空类型,添加专用初始化方法,确保整个Widget生命周期内只创建一次控制器。
  3. 修正dispose执行顺序:先暂停视频播放,再释放控制器资源,确保释放前停止所有播放操作。
  4. 添加状态校验:在构建UI前校验控制器状态,避免未初始化时的错误。

修改后的完整代码

class VideoPlayerApp extends StatefulWidget {
  final String jsonLink;
  final String fileTitle;
  final String userId;
  final String creationTime;
  final bool showOnlyfile;

  const VideoPlayerApp(this.jsonLink, this.fileTitle, this.userId, this.creationTime, this.showOnlyfile, {super.key});
  @override
  State<VideoPlayerApp> createState() => _VideoPlayerAppState();
}

class _VideoPlayerAppState extends State<VideoPlayerApp>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  String videoRootUrl = "";
  VideoPlayerController? videoPlayerController;
  ChewieController? chewieController;
  late Future<String> _videoUrlFuture;

  Future<String> fetchJsonContent() async {
    final jsonResponse = await http.get(Uri.parse(widget.jsonLink));
    if (jsonResponse.statusCode == 200) {
      var jsonContent = jsonDecode(jsonResponse.body);
      videoRootUrl = jsonContent["meta"]["rootUrl"];
      var files = jsonContent["files"];
      if (files.length > 0) {
        return videoRootUrl + files[0];
      }
      return "";
    } else {
      throw Exception('Failed to load json file of drive');
    }
  }

  @override
  void initState() {
    super.initState();
    // 缓存请求Future,避免每次build重复执行网络请求
    _videoUrlFuture = fetchJsonContent();
  }

  @override
  void dispose() {
    print("|||||||||Video Dispose Called|||||||");
    // 先暂停播放,再释放控制器资源
    chewieController?.pause();
    videoPlayerController?.pause();
    chewieController?.dispose();
    videoPlayerController?.dispose();
    super.dispose();
  }

  // 初始化视频控制器,确保仅执行一次
  void _initVideoControllers(String videoUrl) {
    if (videoPlayerController != null) return;

    videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(videoUrl));
    chewieController = ChewieController(
      videoPlayerController: videoPlayerController!,
      autoPlay: false,
      autoInitialize: true,
      looping: false,
      showControls: true,
      aspectRatio: 16 / 9,
      materialProgressColors: const ChewieProgressColors(
        backgroundColor: Color.fromARGB(255, 4, 50, 90),
        bufferedColor: Color.fromARGB(255, 4, 66, 116),
      ),
      errorBuilder: (context, errorMessage) {
        print("Video Player error --- $errorMessage");
        return const Center(
          child: Icon(Icons.error,color: Colors.white,),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return WillPopScope(
      onWillPop: ()async{
        switch(contentUiPreviousPath){
          case "contentList":
            Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (context)=>courseContent(creatorName: currentCourseCreator, courseId: currentCourseId, batchId: currentBatchId, batchTitle: currentBatchTitle, discussionChannel: discussionChannel, announcementChannnel: announcementChannnel, tabBarIndex: 1)));
            break;
          default:
            Navigator.of(context).pop();
            break;
        }
        return false;
      },
      child: Scaffold(
          appBar: (widget.showOnlyfile)?null:const CustomAppBar(networkId: 'No-Network',tempNetworkImgUrl: ""),
          drawer: (widget.showOnlyfile)?null:const CustomDrawer(),
          body: bindVideoFiles()),
    );
  }

  Widget bindVideoFiles() {
    return FutureBuilder(
      future: _videoUrlFuture,
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
        if (snapshot.hasData && snapshot.data!.isNotEmpty) {
          _initVideoControllers(snapshot.data!);

          // 等待控制器初始化完成
          if (videoPlayerController == null || chewieController == null) {
            return const Center(child: CircularProgressIndicator());
          }

          return SingleChildScrollView(
            child: Column(
              children: [
                (widget.showOnlyfile)?const SizedBox():Container(
                        padding: const EdgeInsets.symmetric(
                          vertical: 13,
                        ),
                        width: MediaQuery.of(context).size.width * .9,
                        alignment: Alignment.centerLeft,
                        decoration: const BoxDecoration(
                            border: Border(
                                bottom: BorderSide(
                          width: 2,
                          color: Colors.black54,
                        ))),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              widget.fileTitle,
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                                color: Colors.black45,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                            const SizedBox(
                              height: 3,
                            ),
                            Text(
                              widget.creationTime,
                              style: const TextStyle(
                                fontSize: 12,
                                color: Colors.black45,
                              ),
                              maxLines: 1,
                              textAlign: TextAlign.left,
                              overflow: TextOverflow.ellipsis,
                            ),
                            const SizedBox(
                              height: 3,
                            ),
                          ],
                        ),
                      ),
                Container(
                    height: (MediaQuery.of(context).size.width * 9) / 16,
                    margin: const EdgeInsets.symmetric(vertical: 20),
                    decoration: const BoxDecoration(color: Colors.black),
                    child: Chewie(
                      controller: chewieController!,
                    )),
              ],
            ),
          );
        } else if (snapshot.hasError) {
          return const Center(child: Icon(Icons.error, color: Colors.white));
        } else {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }
}

关键修改说明

  • 缓存Future:_videoUrlFuture在initState中初始化,确保网络请求仅执行一次。
  • 控制器可空化:将videoPlayerController和chewieController改为可空类型,配合_initVideoControllers方法避免重复创建。
  • 修正dispose顺序:先暂停播放再释放控制器,确保资源清理前停止所有播放操作。
  • 状态校验:在FutureBuilder中添加控制器非空判断,防止未初始化时的UI错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 10:24:54