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

Flutter video_player报控制器dispose后仍被使用错误排查

Flutter video_player 连续播放多视频报used after disposed错误修复

问题现象

  • 基于Flutter开发顺序连播的视频应用,实现逻辑为单条视频播放完成后销毁当前VideoPlayerController,再初始化下一条视频的控制器
  • 连续播放4-5条视频时运行正常,播放更多视频后抛出如下异常:
throw FlutterError(
  'A $runtimeType was used after being disposed.\n'
  'Once you have called dispose() on a $runtimeType, it can no longer be used.',
);
  • 当前使用依赖版本:video_player: ^2.4.2

附原问题触发代码:

void playVideo() async {
    if (DateTime.now().isAfter(videoData[index].start_from) &&
        DateTime.now().isBefore(videoData[index].end_on)) {
      bool played_count = await CountQuery().whenComplete(() {
        
      });
      
      file = File(videoData[index].file_link);
      if (file.existsSync() && !played_count) {
        isEnd = true;

        controller = VideoPlayerController.file(file)
          ..initialize().then((value) {
            setState(() {
              controller.play();
            });

            controller.addListener(() {

              // checking the duration and position every time
              // Video Completed//

              if (controller.value.duration == controller.value.position &&
                  isEnd) {
                setState(() {
                  isEnd = false;
                });
                CreateLog("video");                         
                controller.dispose();
                PlayNext();
                
              }
            });
          })
          ..setLooping(false);
      } else {
        PlayNext();
      }
    } else {
      PlayNext();
    }
  }

  void PlayNext() {
    setState(() {
      index++;
    
      if (videoData.length > index) {
        // this.asset = videoData[index].file_link;
        this.file = File(videoData[index].file_link);
        playVideo();
      } else {
        index = 0;
        // this.asset = videoData[index].file_link;
        this.file = File(videoData[index].file_link);
        playVideo();
      }
    });
  }

异常根因

  • 监听器泄漏:每次创建新控制器添加的listener从未被移除,旧控制器被销毁后,残留的监听回调仍可能被系统触发,尝试访问已销毁的控制器实例直接抛错。
  • 异步时序冲突:initialize()是异步耗时操作,视频播放完成触发切源逻辑时,旧控制器的初始化回调、监听回调可能还在事件队列中等待执行,回调执行时控制器已经被dispose,或者全局controller变量已经被替换为新实例,访问时触发异常。
  • 重复触发切源:视频播放到末尾时,position等于duration的状态会连续多次触发listener回调,原逻辑的isEnd状态锁没有完全拦住重复执行,会出现同一个控制器被多次dispose、刚初始化的新控制器被误销毁的情况。
  • 缺少生命周期校验:所有异步操作返回后没有校验当前Widget是否已挂载、控制器是否已销毁,直接执行操作很容易访问到已释放的对象。

修复方案

核心修复点:

  1. 每次销毁控制器前,先移除之前绑定的所有监听回调,避免残留回调执行
  2. 加切源全局锁,视频播放到末尾后立即上锁,避免重复触发dispose和切源逻辑
  3. 所有异步操作(初始化、数据库查询)返回后,二次校验组件挂载状态、切源锁状态,避免执行过期逻辑
  4. 不要在级联操作里直接给全局controller赋值,等初始化完成、校验通过后再赋值,避免半初始化的控制器被其他逻辑访问
  5. 组件销毁时统一释放控制器资源,避免内存泄漏

修复后可运行的核心代码:

// 存储控制器监听回调,用于后续移除
late VoidCallback _videoListener;
// 切源操作锁,避免重复触发切源逻辑
bool _isSwitching = false;
// 全局播放器控制器
VideoPlayerController? controller;

void playVideo() async {
  // 组件已销毁直接终止流程
  if (!mounted) return;

  // 校验当前视频的有效播放时间窗口
  final currentVideo = videoData[index];
  final now = DateTime.now();
  if (now.isBefore(currentVideo.start_from) || now.isAfter(currentVideo.end_on)) {
    PlayNext();
    return;
  }

  final playedCount = await CountQuery();
  // 异步返回后二次校验组件状态、切源状态
  if (!mounted || _isSwitching) return;

  final videoFile = File(currentVideo.file_link);
  if (!videoFile.existsSync() || playedCount) {
    PlayNext();
    return;
  }

  // 先释放上一个控制器的资源
  _isSwitching = true;
  if (controller != null) {
    controller!.removeListener(_videoListener);
    await controller!.dispose();
    controller = null;
  }

  // 创建新控制器,先不要赋值给全局变量
  final newController = VideoPlayerController.file(videoFile)
    ..setLooping(false);

  // 定义监听逻辑
  _videoListener = () {
    // 切源中、控制器未初始化完成直接返回
    if (_isSwitching || !newController.value.isInitialized) return;
    // 用插件内置的isCompleted判断播放完成,避免手动对比的边界问题
    if (newController.value.isCompleted) {
      _isSwitching = true;
      CreateLog("video");
      PlayNext();
    }
  };
  newController.addListener(_videoListener);

  // 初始化新控制器
  await newController.initialize();
  // 初始化完成后再次校验状态,避免初始化过程中已经触发切源
  if (!mounted || _isSwitching) {
    newController.removeListener(_videoListener);
    await newController.dispose();
    return;
  }

  // 状态校验通过,再把新控制器赋值给全局变量
  controller = newController;
  _isSwitching = false;
  setState(() {
    controller!.play();
  });
}

void PlayNext() {
  if (!mounted) return;
  setState(() {
    index++;
    // 播完列表回到第一个
    if (index >= videoData.length) {
      index = 0;
    }
    playVideo();
  });
}

@override
void dispose() {
  _isSwitching = true;
  if (controller != null) {
    controller!.removeListener(_videoListener);
    controller!.dispose();
  }
  super.dispose();
}

优化建议

  • 可以提前初始化下一个要播放的视频控制器,减少切视频时的加载黑屏时长,注意不要同时缓存太多控制器占用内存
  • 切源过程中可以加载过渡loading组件,避免用户操作打断切源流程
  • 可以给视频播放加错误监听,出现播放异常时自动跳过到下一个视频,提升稳定性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 19:27:16