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

Flutter仿Instagram故事切换时视频复用异常问题求助

Flutter仿Instagram故事功能:切换用户后视频重复播放问题解决

问题概述

实现仿Instagram故事功能时,已完成图片和视频故事的基础展示,但存在异常:第一个用户的视频故事播放后,滑动切换到另一个用户的视频故事,暂停再恢复播放时,旧用户的视频会和当前视频一同播放。使用flutter_story插件,尝试销毁VideoPlayerController未解决问题。

问题根源

  1. 全局的storyVideoPlayer流被多个StoryChild实例订阅,切换用户后旧实例的订阅未取消,导致播放命令触发所有已订阅视频同步播放。
  2. Story组件的addAutomaticKeepAlives: true设置,导致旧的StoryChild实例未被销毁,资源和订阅残留。
  3. 所有视频共享同一播放控制流,未实现每个视频独立的播放状态管理。

修复方案

1. 让每个视频StoryChild独立管理播放状态

修改StoryChild组件,取消全局流依赖,通过组件可见性和StoryController状态单独控制每个视频的播放:

class _StoryChildState extends State<StoryChild> {
  StreamSubscription? _storyControllerSubscription;
  bool _isInitialized = false;
  VideoPlayerController? videoPlayer;
  bool _isVisible = false;

  @override
  void initState() {
    super.initState();
    if (widget.story?.type == "video") {
      _initializeVideo();
      _listenToStoryController();
    }
  }

  Future<void> _initializeVideo() async {
    final file = await VideoCacheManager.getVideoFile(widget.story?.cdnUrl ?? '');
    videoPlayer = VideoPlayerController.file(file);
    await videoPlayer?.initialize().then((value) {
      if (mounted) {
        setState(() {
          _isInitialized = true;
        });
      }
      // 初始可见时自动播放
      if (_isVisible) {
        videoPlayer?.play();
      }
    });
  }

  void _listenToStoryController() {
    _storyControllerSubscription = widget.storyController?.playbackStream.listen((state) {
      if (videoPlayer == null) return;
      if (state == StoryPlaybackState.play) {
        if (_isVisible) {
          videoPlayer?.play();
        }
      } else if (state == StoryPlaybackState.pause) {
        videoPlayer?.pause();
      }
    });
  }

  void _disposeResources() {
    videoPlayer?.pause();
    videoPlayer?.dispose();
    videoPlayer = null;
    _storyControllerSubscription?.cancel();
    _storyControllerSubscription = null;
    _isInitialized = false;
  }

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

  @override
  Widget build(BuildContext context) {
    if (widget.story?.type == "image") {
      return Stack(
        alignment: Alignment.center,
        fit: StackFit.expand,
        children: [
          Positioned.fill(child: Container(color: Colors.transparent)),
          CachedNetworkImage(
            imageUrl: widget.story?.cdnUrl ?? '',
            fit: BoxFit.contain,
            alignment: Alignment.center,
            cacheKey: widget.story?.cdnUrl ?? '',
            cacheManager: CustomCacheManager().getCacheManager(widget.story?.cdnUrl ?? ''),
            progressIndicatorBuilder: (context, url, progress) => Center(
              child: CircularProgressIndicator(color: Colors.white),
            ),
          ),
        ],
      );
    } else if (widget.story?.type == "video") {
      return getContentView();
    } else {
      return Container(
        decoration: BoxDecoration(
          color: Color(int.parse(widget.story?.backgroundColor ?? '')),
        ),
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      );
    }
  }

  Widget getContentView() {
    if (_isInitialized) {
      return VisibilityDetector(
        key: Key(widget.story?.cdnUrl ?? ''),
        onVisibilityChanged: (info) {
          setState(() {
            _isVisible = info.visibleFraction == 1;
          });
          if (videoPlayer == null) return;
          if (_isVisible) {
            if (widget.storyController?.isPlaying ?? false) {
              videoPlayer?.play();
            }
          } else {
            videoPlayer?.pause();
          }
        },
        child: Center(
          child: ClipRRect(
            borderRadius: BorderRadius.all(Radius.circular(10)),
            child: AspectRatio(
              aspectRatio: videoPlayer!.value.aspectRatio,
              child: VideoPlayer(videoPlayer!),
            ),
          ),
        ),
      );
    } else {
      return Center(
        child: SizedBox(
          width: 70,
          height: 70,
          child: CircularProgressIndicator(
            valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            strokeWidth: 3,
          ),
        ),
      );
    }
  }
}

2. 关闭父组件的自动保活

修改Story组件的addAutomaticKeepAlives为false,确保切换用户时旧组件被销毁:

Story(
  sortByVisited: true,
  shrinkWrap: true,
  autoplay: true,
  physics: NeverScrollableScrollPhysics(),
  controller: controller.storyController,
  scrollDirection: Axis.horizontal,
  addAutomaticKeepAlives: false, // 修改此处
  addRepaintBoundaries: false,
  // 其他属性保持不变
)

3. 简化StoryCard的暂停/恢复逻辑

不再依赖全局storyVideoPlayer,直接通过StoryController控制:

onPause: (value) {
  controller.storyController.pauseStory();
},
onResume: (value) {
  controller.storyController.playStory();
},

修复原理

  • 每个视频独立创建VideoPlayerController,避免实例共享冲突。
  • 通过VisibilityDetector监听组件可见性,仅当前可见的视频响应播放命令,不可见时自动暂停。
  • 取消全局流的多订阅问题,改为监听StoryController状态,确保播放操作仅作用于当前可见视频。
  • 关闭自动保活,确保切换用户时旧组件资源被彻底释放。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:42:01