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

Flutter使用video_compress二次压缩报错:Bad state: Stream已被监听

问题:Flutter中使用video_compress二次压缩时抛出Bad state: Stream has already been listened to.错误

我在Flutter应用里用video_compress包实现视频压缩功能,流程是用户选完视频后,跳转下一页前先执行压缩,压缩时显示带进度的对话框。第一次压缩正常,进度对话框也能正确显示,但第二次及之后压缩会抛出Bad state: Stream has already been listened to.错误,求问题原因及解决办法。

GalleryApi类代码

class GalleryApi {
  static Future<MediaInfo?> videoCompress(List<File?> file) async {
    try {
      await VideoCompress.setLogLevel(0);

      return VideoCompress.compressVideo(file.last!.path,
          quality: VideoQuality.LowQuality, includeAudio: true);
    } catch (e) {
      VideoCompress.cancelCompression();
    }
  }
}

首屏压缩方法代码

Future compressVideo() async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Dialog(child: ProgressDialogWidget()),
    );

    final info = await GalleryApi.videoCompress(selectedAssets.value);

    Navigator.of(context).pop();

    final size = await selectedAssets.value.last!.lengthSync();
    final ogSize = (size / 1000000).round();

    selectedAssets.value.clear();
    selectedAssets.value.add(info!.file);

    Get.to(ImagePost(
      images: selectedAssets.value,
      isVideo: isVideo,
      ogSize: ogSize.toString(),
      blurHash: [],
    ));
  }

进度对话框代码

class _ProgressDialogWidgetState extends State<ProgressDialogWidget> {
  late Subscription subscription;
  double? progress;

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

    subscription = VideoCompress.compressProgress$
        .subscribe((progress) => setState(() => this.progress = progress));
  }

  @override
  void dispose() {
    VideoCompress.cancelCompression();
    subscription.unsubscribe;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final value = progress == null ? progress : progress! / 100;
    final _height = MediaQuery.of(context).size.height;
    final _width = MediaQuery.of(context).size.width;
    return SizedBox(
      height: _height * 0.2,
      width: _width,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "Please wait while it's compressing...",
              style: TextStyle(fontSize: _width * 0.03),
            ),
            SizedBox(
              height: _height * 0.02,
            ),
            Row(
              children: [
                Expanded(
                  child: LinearProgressIndicator(
                    value: value,
                    minHeight: _height * 0.015,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

问题原因

  • VideoCompress.compressProgress$是单订阅流(Single-Subscription Stream),这类流只能被监听一次,首次监听完成后流会自动关闭,二次监听时就会触发Bad state错误。
  • 每次打开ProgressDialogWidget都会重新订阅该流,且dispose方法中存在语法错误:subscription.unsubscribe缺少括号,导致首次订阅未被正确取消,流资源未释放。

解决方案

  1. 修复订阅取消的语法错误
    在ProgressDialogWidget的dispose方法中,将subscription.unsubscribe改为subscription.unsubscribe();,确保对话框销毁时正确取消订阅:

    @override
    void dispose() {
      VideoCompress.cancelCompression();
      subscription.unsubscribe(); // 补全括号
      super.dispose();
    }
    
  2. 将单订阅流转为多订阅流
    使用asBroadcastStream()将compressProgress$转换为广播流,支持多次监听,修改initState中的订阅代码:

    @override
    void initState() {
      super.initState();
      subscription = VideoCompress.compressProgress$
          .asBroadcastStream() // 转为广播流
          .subscribe((progress) => setState(() => this.progress = progress));
    }
    
  3. 额外优化建议

    • 每次压缩开始前,可主动取消之前可能存在的订阅,避免流资源占用;
    • 在GalleryApi.videoCompress方法中,压缩完成或异常时,可调用VideoCompress.cancelCompression()清理流状态,确保下次压缩时流处于可用状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 21:47:18