wang-bin / fvp

Flutter video player plugin for all desktop+mobile platforms. download prebuilt examples from github actions. https://pub.dev/packages/fvp
BSD 3-Clause "New" or "Revised" License
126 stars 20 forks source link

Crash on iOS #79

Closed scottynoshotty closed 2 months ago

scottynoshotty commented 2 months ago

VideoPlayerController.initialize.eventListener FlutterError - Bad state: Future already completed package:video_player/video_player.dart:454 Repetitive crashes

I am using FVP to override the iOS video player in my app. I have been seeing the above error in my Firebase Crashlytics consistently since I started using FVP (only occurs on iOS devices). This does not appear to result in any user visible issue but it is a repetitive crash.

wang-bin commented 2 months ago

please paste the log and more crash stacks because I have no ios device to test today

scottynoshotty commented 2 months ago

Hey I haven't been able to repro this yet, just seeing it in Crashlytics.

scottynoshotty commented 2 months ago

Hi Wang,

Just wanted to add more context, this has been a recurring crash in my Crashlytics since I brought in FVP. Only happens on iOS devices (I use Flutter default video player for Android). If you need any sample code let me know, I haven't repro'd locally.

scottynoshotty commented 2 months ago

Here is the code I am using to play the video

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:social_app/app/app.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:video_player/video_player.dart';

class PostVideo extends StatefulWidget {
  final Post post;
  final PostVideoController postVideoController;

  const PostVideo({
    super.key,
    required this.post,
    required this.postVideoController,
  });

  @override
  State<PostVideo> createState() => _PostVideoState();
}

class _PostVideoState extends State<PostVideo> {
  late final VideoPlayerController _videoController;
  bool _videoReady = false;

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

  Future<void> initVideo() async {
    Blob videoBlob = widget.post.blobs[1];
    if (videoBlob.isPending && videoBlob.localFilePath.isNotEmpty) {
      _videoController = VideoPlayerController.file(
        File(
          videoBlob.localFilePath,
        ),
      );
    } else {
      _videoController = VideoPlayerController.networkUrl(
        Uri.parse(
          widget.post.blobs[1].hdMp4Url,
        ),
      );
    }
    await _videoController.initialize();
    widget.postVideoController._attach(_videoController);
    widget.postVideoController._notifyPlaybackReady();
    WakelockPlus.enable();
    setState(() {
      _videoReady = true;
    });
  }

  @override
  void dispose() async {
    super.dispose();
    await _videoController.dispose();
    widget.postVideoController.detach();
    WakelockPlus.disable();
  }

  @override
  Widget build(BuildContext context) {
    if (_videoReady) {
      return _getVideoWidget();
    } else {
      return _getWaitingForVideoWidget();
    }
  }

  Widget _getWaitingForVideoWidget() {
    return Container(
      width: double.infinity,
      child: AspectRatio(
        aspectRatio: widget.post.blobs[1].aspectRatio,
        child: Container(
          color: Theme.of(context).colorScheme.background,
        ),
      ),
    );
  }

  Widget _getVideoWidget() {
    return Container(
      width: double.infinity,
      child: GestureDetector(
        onTap: widget.postVideoController.playOrPause,
        child: AspectRatio(
          aspectRatio: widget.post.blobs[1].aspectRatio,
          child: VideoPlayer(
            _videoController,
          ),
        ),
      ),
    );
  }
}

class PostVideoController {
  VideoPlayerController? _controller;
  VoidCallback? onPlaybackReady;

  PostVideoController(VoidCallback onPlaybackReady) {
    this.onPlaybackReady = onPlaybackReady;
  }

  void _notifyPlaybackReady() {
    if (onPlaybackReady != null) {
      onPlaybackReady!();
    }
  }

  void _attach(VideoPlayerController controller) {
    _controller = controller;
  }

  void detach() {
    _controller = null;
  }

  void play() {
    if (_controller != null && !_controller!.value.isPlaying) {
      _controller!.play();
    }
  }

  void pause() {
    if (_controller != null && _controller!.value.isPlaying) {
      _controller!.pause();
    }
  }

  void playOrPause() {
    if (_controller == null) {
      return;
    }
    if (_controller!.value.isPlaying) {
      pause();
    } else {
      play();
    }
  }
}
scottynoshotty commented 2 months ago

Also it looks like this issue is happening very regularly and is resulting in user visible crashes.

wang-bin commented 2 months ago

stack trace is required to analyze the crash. btw I fixed a crash issue, in your project dir run

flutter clean
pod cache clean mdk
find . -name Podfile.lock -delete

then build again

scottynoshotty commented 2 months ago

I will work on getting a stack trace and get back to you.

scottynoshotty commented 2 months ago
flutter: Bad state: Future already completed
flutter:
#0      _AsyncCompleter.complete (dart:async/future_impl.dart:43:31)
#1      VideoPlayerController.initialize.eventListener (package:video_player/video_player.dart:454:33)
#2      _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10)
#3      _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
#4      _DelayedData.perform (dart:async/stream_impl.dart:515:14)
#5      _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11)
#6      _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:591:7)
#7      _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
#8      _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
scottynoshotty commented 2 months ago

I have figured out how to reproduce this.

In my app I click on a video post and it plays without issue. At the end of the video the playback stops (not set to loop mode). To replay the video you just tap the video and it calls

  void play() {
    if (_controller != null && !_controller!.value.isPlaying) {
      _controller!.play();
    }
  }

and that starts the video from the beginning. When the video restarts the above stacktrace logs from the error that is thrown. The video still plays without issue and there does not appear to be any user visible problems.

This only happens when playing a video for the second time. The video loads fine initially and plays through without any exception being thrown.

scottynoshotty commented 2 months ago

It seems like the root issue is calling play() on a controller attached to a video which is already initialized

wang-bin commented 2 months ago

yes. official ios/android implementation becomes paused state and resources are not released when reaches end, initialized event is sent only once. but fvp releases internal resources when playback finished, calling play() again will initialize resources and send event again.

try fvp master branch code, the event is sent only once.

scottynoshotty commented 2 months ago

Thanks Wang, I am installing and pushing out to devices today. I will let you know if the issue continues.

scottynoshotty commented 2 months ago

Hey, looking at Crashlytics it looks like the error is fixed. Thanks!