fluttercommunity / chewie

The video player for Flutter with a heart of gold
MIT License
1.94k stars 1k forks source link

A PlayerNotifier was used after being disposed. this happened when i rotate device in fullscreen mode #857

Closed AhmedAlaaGenina closed 2 weeks ago

AhmedAlaaGenina commented 3 weeks ago

**I create class to handle player that get link of youtube using youtube_explode_dart package then get link to show in my player

her is my code**

import 'dart:async';
import 'dart:developer';

import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';

/// Represents the current status of the YPlayer.
enum PlayerStatus { initial, loading, playing, paused, stopped, error }

class YouTubePlayerController extends ChangeNotifier {
  YouTubePlayerController({
    required this.youtubeUrl,
    this.autoPlay = true,
    this.allowFullScreen = true,
    this.aspectRatio,
    this.allowMuting = true,
    this.placeholder,
    this.overlay,
    this.loadingWidget,
    this.errorWidget,
    this.materialProgressColors,
    this.cupertinoProgressColors,
  }) {
    initialize();
  }

  // Player configuration properties
  final bool allowFullScreen;
  final bool allowMuting;
  final double? aspectRatio;
  final bool autoPlay;
  final ChewieProgressColors? cupertinoProgressColors;
  final Widget? errorWidget;
  final Widget? loadingWidget;
  final ChewieProgressColors? materialProgressColors;
  final Widget? placeholder;
  final Widget? overlay;
  final String youtubeUrl;

  // Internal properties
  VideoPlayerController? _videoPlayerController;
  ChewieController? _chewieController;
  PlayerStatus _playerStatus = PlayerStatus.initial;
  List<VideoQuality> _qualities = [];
  final YoutubeExplode _yt = YoutubeExplode();
  VideoQuality? _currentQuality;

  // Add a StreamController for custom events
  final StreamController<Map<String, dynamic>> _eventsController =
      StreamController<Map<String, dynamic>>.broadcast();

  Stream<Map<String, dynamic>> get onPlayerEvent => _eventsController.stream;

  @override
  void dispose() {
    if (_videoPlayerController != null) _videoPlayerController?.dispose();
    _yt.close();
    _eventsController.close();
    if (_chewieController != null) _chewieController?.dispose();
    super.dispose();
  }

  ChewieController? get chewieController => _chewieController;

  List<VideoQuality> get qualities => _qualities;

  /// Gets the current status of the player.
  PlayerStatus get playerStatus => _playerStatus;

  /// Gets the current playback position.
  Duration get position =>
      _chewieController?.videoPlayerController.value.position ?? Duration.zero;

  /// Gets the total duration of the video.
  Duration get duration =>
      _chewieController?.videoPlayerController.value.duration ?? Duration.zero;

  VideoQuality? get currentQuality => _currentQuality;

  /// Initializes the video player.
  Future<void> initialize() async {
    _setPlayerStatus(PlayerStatus.loading);
    try {
      final video = await _yt.videos.get(youtubeUrl);
      final manifest = await _yt.videos.streamsClient.getManifest(video.id);
      final streams = manifest.muxed;

      _qualities =
          streams.map((s) => VideoQuality(s.qualityLabel, s.url)).toList()
            ..sort((a, b) => int.parse(b.label.replaceAll(RegExp(r'[^\d]'), ''))
                .compareTo(int.parse(a.label.replaceAll(RegExp(r'[^\d]'), ''))))
            ..toSet().toList(); // Remove duplicates

      final highestQualityStream = streams.withHighestBitrate();
      _qualities.add(VideoQuality("Auto", highestQualityStream.url));
      _currentQuality = _qualities.last;
      _videoPlayerController =
          VideoPlayerController.networkUrl(_currentQuality!.url);

      await _videoPlayerController!.initialize();
      _initializeChewieController();
      _chewieController?.videoPlayerController.addListener(_videoListener);
      _setPlayerStatus(PlayerStatus.playing);
    } catch (e) {
      _setPlayerStatus(PlayerStatus.error);
      log("Error initializing video: $e");
    }
    notifyListeners();
  }

  /// Starts or resumes video playback.
  void play() {
    _chewieController?.videoPlayerController.play();
  }

  /// Pauses video playback.
  void pause() {
    _chewieController?.videoPlayerController.pause();
  }

  /// Stops video playback and resets to the beginning.
  void stop() {
    _chewieController?.videoPlayerController.pause();
    _chewieController?.videoPlayerController.seekTo(Duration.zero);
  }

  /// Seeks to a specific position in the video.
  void seekTo(Duration position) {
    _chewieController?.videoPlayerController.seekTo(position);
  }

  /// Changes the video quality.
  Future<void> changeQuality(VideoQuality quality) async {
    _setPlayerStatus(PlayerStatus.loading);
    try {
      final currentPosition =
          _chewieController!.videoPlayerController.value.position;
      await _chewieController?.videoPlayerController.dispose();
      _videoPlayerController = VideoPlayerController.networkUrl(quality.url);
      await _videoPlayerController!.initialize();
      seekTo(currentPosition);
      _chewieController?.dispose();
      _initializeChewieController();
      _chewieController?.videoPlayerController.addListener(_videoListener);
      _setPlayerStatus(PlayerStatus.playing);
      _currentQuality = quality;
    } catch (e) {
      _setPlayerStatus(PlayerStatus.error);
      log("Error changing quality of video: $e");
    }
    notifyListeners();
  }

  /// Displays a dialog for selecting video quality.
  void showQualityDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Select Quality'),
          content: SingleChildScrollView(
            child: ListBody(
              children: _qualities.map((quality) {
                return ListTile(
                  title: Text(quality.label),
                  trailing: quality == _currentQuality
                      ? const Icon(Icons.check)
                      : null,
                  onTap: () {
                    changeQuality(quality);
                    Navigator.of(context).pop();
                  },
                );
              }).toList(),
            ),
          ),
        );
      },
    );
  }

  /// Video listener to update player status.
  void _videoListener() {
    final playerValue = _chewieController!.videoPlayerController.value;
    if (playerValue.isPlaying) {
      _setPlayerStatus(PlayerStatus.playing);
      _eventsController.add(
          {'event': PlayerStatus.playing, 'position': playerValue.position});
    } else if (playerValue.position >= playerValue.duration) {
      _setPlayerStatus(PlayerStatus.stopped);
      _eventsController.add({'event': PlayerStatus.stopped});
    } else {
      _setPlayerStatus(PlayerStatus.paused);
      _eventsController.add(
          {'event': PlayerStatus.paused, 'position': playerValue.position});
    }
  }

  /// Sets the player status and notifies listeners.
  void _setPlayerStatus(PlayerStatus newStatus) {
    if (_playerStatus != newStatus) {
      _playerStatus = newStatus;
      _eventsController
          .add({'event': 'statusChanged', 'status': newStatus.toString()});
      notifyListeners();
    }
  }

  /// Initializes the Chewie controller.
  void _initializeChewieController() {
    _chewieController = ChewieController(
      videoPlayerController: _videoPlayerController!,
      placeholder: placeholder,
      overlay: overlay,
      autoPlay: autoPlay,
      looping: false,
      aspectRatio: aspectRatio ?? (chewieController?.aspectRatio ?? 16 / 9),
      allowFullScreen: allowFullScreen,
      allowMuting: allowMuting,
      showControls: true,
      allowedScreenSleep: false,
      deviceOrientationsAfterFullScreen: [
        DeviceOrientation.landscapeRight,
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.portraitUp,
        DeviceOrientation.portraitDown,
      ],
      autoInitialize: true,
      materialProgressColors: materialProgressColors ?? ChewieProgressColors(),
      cupertinoProgressColors:
          cupertinoProgressColors ?? ChewieProgressColors(),
      additionalOptions: (context) {
        return <OptionItem>[
          OptionItem(
            onTap: () => showQualityDialog(context),
            iconData: Icons.hd,
            title: 'Quality',
          ),
        ];
      },
      errorBuilder: (context, errorMessage) {
        return Center(
          child: errorWidget ?? Text(errorMessage),
        );
      },
    );
  }
}

class YouTubePlayer extends StatelessWidget {
  const YouTubePlayer({super.key, required this.controller});

  final YouTubePlayerController controller;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final aspectRatio = controller.aspectRatio ??
            (controller.chewieController?.aspectRatio ?? 16 / 9);
        final playerWidth = constraints.maxWidth;
        final playerHeight = playerWidth / aspectRatio;

        if (controller.chewieController != null) {
          // Video is ready to play
          return SizedBox(
            width: playerWidth,
            height: playerHeight,
            child: FittedBox(
              fit: BoxFit.contain,
              child: SizedBox(
                width: playerWidth,
                height: playerHeight,
                child: Chewie(controller: controller.chewieController!),
              ),
            ),
          );
        } else if (controller.playerStatus == PlayerStatus.loading) {
          // Show loading widget
          return _buildLoadingWidget(playerHeight, playerWidth);
        } else if (controller.playerStatus == PlayerStatus.error) {
          // Show error widget
          return _buildErrorWidget(playerHeight, playerWidth);
        } else {
          // Default empty container
          return const SizedBox.shrink();
        }
      },
    );
  }

  Widget _buildLoadingWidget(double height, double width) {
    return SizedBox(
      height: height,
      width: width,
      child: Center(
        child: controller.loadingWidget ??
            const CircularProgressIndicator.adaptive(),
      ),
    );
  }

  Widget _buildErrorWidget(double height, double width) {
    return SizedBox(
      height: height,
      width: width,
      child: Center(
        child: controller.errorWidget ?? const Text('Error loading video'),
      ),
    );
  }
}

class VideoQuality {
  VideoQuality(this.label, this.url);

  final String label;
  final Uri url;

  @override
  bool operator ==(covariant VideoQuality other) {
    if (identical(this, other)) return true;

    return other.label == label && other.url == url;
  }

  @override
  int get hashCode => label.hashCode ^ url.hashCode;
}

and I use the code in provider like this

late YouTubePlayerController ytController;

  void startVideo(String videoUrl) async {
    ytController = YouTubePlayerController(
      youtubeUrl: videoUrl,
      overlay: Positioned.fill(
        child: LayoutBuilder(
          builder: (context, constraints) {
            return BouncingAnimationWidget(
              speed: 1,
              screenHeight: constraints.maxHeight,
              screenWidth: constraints.maxWidth,
              child: IgnorePointer(
                ignoring: true,
                child: Text(
                  "${Provider.of<AutheProvider>(context, listen: false).usersModel.name}\n${Provider.of<AutheProvider>(context, listen: false).usersModel.code}",
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: AppColors.mainWhite.withOpacity(0.6),
                    fontSize: 24,
                    shadows: const [
                      Shadow(
                        color: Colors.grey,
                        blurRadius: 2.0,
                        offset: Offset(2.0, 2.0),
                      ),
                    ],
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
    ytController.onPlayerEvent.listen((event) {
      bool fullScreen = ytController.chewieController!.isFullScreen;

      Future.delayed(const Duration(milliseconds: 500), () {
        isShowWatermarkFullScreen = fullScreen;
      });
      isFullScreen = fullScreen;
      int stopWatchedTime = 0;
      ttsStopwatch.start();
      stopWatchedTime = (ttsStopwatch.elapsedMilliseconds / 1000).round();
      if (stopWatchedTime >= 10) {
        ttsStopwatch.reset();
        log("stopWatchedTime: $stopWatchedTime");
        speakCode();
      }
      if (ytController.playerStatus != PlayerStatus.playing) {
        ttsStopwatch.reset();
      }
      notifyListeners();
    });
  }

**and my ui call startVideo() in my didChangeDependencies()

and call player in UI**

YouTubePlayer(controller: videosProvider.ytController),

Full error

    I/flutter (27644): Once you have called dispose() on a PlayerNotifier, it can no longer be used.
I/flutter (27644): #0      ChangeNotifier.debugAssertNotDisposed.<anonymous closure> (package:flutter/src/foundation/change_notifier.dart:183:9)
I/flutter (27644): #1      ChangeNotifier.debugAssertNotDisposed (package:flutter/src/foundation/change_notifier.dart:190:6)
I/flutter (27644): #2      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:416:27)
I/flutter (27644): #3      PlayerNotifier.hideStuff= (package:chewie/src/notifiers/player_notifier.dart:19:5)
I/flutter (27644): #4      _MaterialControlsState._startHideTimer.<anonymous closure>.<anonymous closure> (package:chewie/src/material/material_controls.dart:621:18)
I/flutter (27644): #5      State.setState (package:flutter/src/widgets/framework.dart:1203:30)
I/flutter (27644): #6      _MaterialControlsState._startHideTimer.<anonymous closure> (package:chewie/src/material/material_controls.dart:620:7)
I/flutter (27644): #7      _rootRun (dart:async/zone.dart:1391:47)
I/flutter (27644): #8      _CustomZone.run (dart:async/zone.dart:1301:19)
I/flutter (27644): #9      _CustomZone.runGuarded (dart:async/zone.dart:1209:7)
I/flutter (27644): #10     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1249
I/flutter (27644): ----------------------------------------------------

can anyone help with this i searched about that for 2 days any help please

Rioverde commented 2 weeks ago

I have the same issue !

AhmedAlaaGenina commented 2 weeks ago

I Solved it.

my Issue is when I rotate the device when i in full screen i have build another widget that have the player and this scenario make old widget dispose and new one created i solve this issue and make it one player every thing work fine

class PersistentVideoPlayer extends StatefulWidget {
  final VideosModel videosModel;

  const PersistentVideoPlayer({
    super.key,
    required this.videosModel,
  });

  @override
  State<PersistentVideoPlayer> createState() => PersistentVideoPlayerState();
}

class PersistentVideoPlayerState extends State<PersistentVideoPlayer>
    with AutomaticKeepAliveClientMixin {
  late YPlayer videoPlayer;

  @override
  bool get wantKeepAlive => true;

  @override
  void initState() {
    super.initState();
    videoPlayer = YPlayer(
      controller: context.read<VideosProvider>().ytController,
    );
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return videoPlayer;
  }
}

class VideoPdfWidget extends StatefulWidget {
  const VideoPdfWidget({
    super.key,
    required this.videosModel,
  });

  final VideosModel videosModel;

  @override
  State<VideoPdfWidget> createState() => _VideoPdfWidgetState();
}

class _VideoPdfWidgetState extends State<VideoPdfWidget> {
  late final GlobalKey<PersistentVideoPlayerState> videoPlayerKey;

  @override
  void initState() {
    super.initState();
    videoPlayerKey = GlobalKey<PersistentVideoPlayerState>();
    context.read<VideosProvider>().initMultiSplitViewController(
          videosModel: widget.videosModel,
          videoPlayerKey: videoPlayerKey,
        );
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<VideosProvider>(
      builder: (context, videosProvider, child) {
        return OrientationBuilder(
          builder: (context, orientation) {
            final isLandscape = orientation == Orientation.landscape;
            final isMobile = context.isMobile;

            if (!isLandscape || isMobile) {
              return VideoContentLayout(
                videosModel: widget.videosModel,
                videoPlayerKey: videoPlayerKey,
              );
            }

            return MultiSplitViewTheme(
              data: MultiSplitViewThemeData(
                dividerPainter: DividerPainters.grooved1(
                    color: Colors.indigo[100]!,
                    highlightedColor: Colors.indigo[900]!),
              ),
              child: MultiSplitView(
                controller: videosProvider.multiSplitViewController,
              ),
            );
          },
        );
      },
    );
  }
}

class VideoContentLayout extends StatelessWidget {
  final VideosModel videosModel;
  final GlobalKey<PersistentVideoPlayerState> videoPlayerKey;

  const VideoContentLayout({
    super.key,
    required this.videosModel,
    required this.videoPlayerKey,
  });

  @override
  Widget build(BuildContext context) {
    return Consumer2<VideosProvider, CommentsProvider>(
      builder: (context, videosProvider, commentsProvider, child) {
        return SingleChildScrollView(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              PersistentVideoPlayer(
                key: videoPlayerKey,
                videosModel: videosModel,
              ),
              Visibility(
                visible: !commentsProvider.showComments,
                replacement: CommentsSection(
                  commentsProvider: commentsProvider,
                  videosModel: videosModel,
                ),
                child: RattingButtonsSection(
                  commentsProvider: commentsProvider,
                  videosModel: videosModel,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}