media-kit / media-kit

A cross-platform video player & audio player for Flutter & Dart.
https://github.com/media-kit/media-kit
MIT License
893 stars 126 forks source link

exit full screen in customized video controls #768

Closed b2nil closed 4 weeks ago

b2nil commented 1 month ago

I tried to implement a simple video controls using the provided custom builder for video controls, and used showModalBottomSheet to implement the bottom bar for housing the play/pause button, the seekbar and the fullscreen button. To control the fullscreen state, I used a global key as suggested in the docs.

Video(
     key: key,
     controller: controller,
     controls: (state) {
         state.widget.controller.player.stream.buffering.listen((event) {
             if (buffering != event) {
                setState(() {
                  buffering = event;
                });
             }
         });

         state.widget.controller.player.stream.playing.listen((event) {
          if (playing != event) {
               setState(() {
                  playing = event;
               });
             }
         });

         return Scaffold(
            backgroundColor: Colors.transparent,
            body: Builder(
                builder: (ctx) => GestureDetector(
                  // _toggleFullscreen here works with double tap
                  // okay to enter and exit fullscreen as expected
                  onDoubleTap: () {
                    _toggleFullscreen(() {
                        setState(() {
                            isFullscreen = !isFullscreen;
                        });
                    });
                    // key.currentState?.toggleFullscreen();
                  },
                  onTap: () {
                    _showVideoControls(ctx);
                  },
                  child: _buildLoadingIndicator(),
                ),
            ),
        );
     },
),

The issue I met is that, inside the bottom sheet created using showModalBottomSheet, when I clicked the fullscreen button, the player can enter fullscreen mode, but cannot exit when clicking the fullscreen button again. I used the builtin toggleFullscreen method through the global key, but I didn't use the StreamBuilder for the Icon implementation.

Here is the _toggleFullscreen and _showVideoControls implementation using a bottom sheet:

  // exitFullscreen not working as expected
  void _toggleFullscreen(VoidCallback onToggle) async {
    await key.currentState?.toggleFullscreen();

    // if (!isFullscreen) {
    //   await key.currentState?.enterFullscreen();
    // } else {
    //   await key.currentState?.exitFullscreen();
    // }

    // tried to set the `isFullscreen` with and without a callback function
    onToggle();

    // setState(() {
    //   isFullscreen = !isFullscreen;
    // });
  }

  void _showVideoControls(BuildContext context) {
    const controlsColor = Colors.white;
    showModalBottomSheet(
      context: context,
      backgroundColor: const Color.fromARGB(122, 14, 14, 14),
      useSafeArea: true,
      constraints: BoxConstraints.expand(
        width: MediaQuery.of(context).size.width,
        height: 60,
      ),
      // Using StatefulBuilder here to get access to the inner `setState` method
      // so that the icons updates accordingly with the changes of
      // the states `playing` and `isFullscreen`
      builder: (builderContext) => StatefulBuilder(
        builder: (ctx, innerSetState) => LayoutBuilder(
          builder: (layoutCtx, constraints) => SizedBox(
            child: Center(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,
                children: [
                  IconButton(
                    onPressed: () {
                      innerSetState(() {
                        playPause();
                      });
                    },
                    icon: Icon(
                      playing ? Icons.pause : Icons.play_arrow,
                      color: controlsColor,
                    ),
                  ),
                  const Expanded(
                    child: Padding(
                      padding: EdgeInsets.all(20.0),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: [
                          LinearProgressIndicator(),
                        ],
                      ),
                    ),
                  ),
                  IconButton(
                    // The issue is here:
                    //  - okay to enter fullscreen when the button is clicked
                    //  - NOT okay to exit fullscreen when the button is clicked
                    onPressed: () {
                      // direct use of this did not work as well
                      // key.currentState?.toggleFullscreen();
                      _toggleFullscreen(() {
                        innerSetState(() {
                          isFullscreen = !isFullscreen;
                        });
                      });
                    },
                    icon: Icon(
                      // The icon toggles as expected when the button is clicked
                      isFullscreen
                          ? Icons.fullscreen_exit
                          : Icons.fullscreen,
                      color: controlsColor,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

The thing that puzzles me is that I can toggle the fullscreen mode as expected by double tap on the player, and I can enter fullscreen by just clicking the fullscreen icon button, but I cannot exit fullscreen mode by just clicking the fullscreen icon button. The _toggleFullscreen funtion fires as expected at debugging.

The builtin MaterialVideoControls works as expectd. So I guess maybe I did something incorrectly which led to this issue, but I failed to spot. Would you guys please kindly have a look at my code and let me known what went wrong?

Here is my development environment:

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.19.5, on Microsoft Windows [Version 10.0.19045.4239], locale zh-CN)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.4.2)
[√] Android Studio (version 2023.2)
[√] VS Code, 64-bit edition (version 1.88.0)
[√] Connected device (3 available)
[√] Network resources

• No issues found!

And here below is a fullly runnable code to reproduce the issue:

Click to expand! ```dart import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); runApp(const TVApp()); } class TVApp extends StatelessWidget { const TVApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'IPTV Player', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color.fromARGB(255, 47, 99, 89)), useMaterial3: true, ), home: const DemoPlayer(title: 'Demo Mediakit Player'), ); } } class DemoPlayer extends StatefulWidget { const DemoPlayer({super.key, required this.title}); final String title; @override State createState() => _DemoPlayerState(); } class _DemoPlayerState extends State { late final GlobalKey key = GlobalKey(); late final player = Player(); late var controller = VideoController(player); bool playing = false; bool buffering = false; bool isFullscreen = false; @override void initState() { super.initState(); if (mounted) { controller = VideoController(player); player.open(Media("http://live-hls-web-aje.getaj.net/AJE/01.m3u8")); setState(() { isFullscreen = key.currentState?.isFullscreen() ?? false; }); } } // exitFullscreen not working as expected void _toggleFullscreen(VoidCallback onToggle) async { await key.currentState?.toggleFullscreen(); // if (!isFullscreen) { // await key.currentState?.enterFullscreen(); // } else { // await key.currentState?.exitFullscreen(); // } // tried to set the `isFullscreen` with and without a callback function onToggle(); // setState(() { // isFullscreen = !isFullscreen; // }); } void playPause() { setState(() { controller.player.playOrPause(); playing = !playing; }); } void _showVideoControls(BuildContext context) { const controlsColor = Colors.white; showModalBottomSheet( context: context, backgroundColor: const Color.fromARGB(122, 14, 14, 14), useSafeArea: true, constraints: BoxConstraints.expand( width: MediaQuery.of(context).size.width, height: 60, ), // Using StatefulBuilder here to get access to the inner `setState` method // so that the icons updates accordingly with the changes of // the states `playing` and `isFullscreen` builder: (builderContext) => StatefulBuilder( builder: (ctx, innerSetState) => LayoutBuilder( builder: (layoutCtx, constraints) => SizedBox( child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () { innerSetState(() { playPause(); }); }, icon: Icon( playing ? Icons.pause : Icons.play_arrow, color: controlsColor, ), ), const Expanded( child: Padding( padding: EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ LinearProgressIndicator(), ], ), ), ), IconButton( // The issue is here: // - okay to enter fullscreen when the button is clicked // - NOT okay to exit fullscreen when the button is clicked onPressed: () { _toggleFullscreen(() { innerSetState(() { isFullscreen = !isFullscreen; }); }); }, icon: Icon( // The icon toggles as expected when the button is clicked isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, color: controlsColor, ), ), ], ), ), ), ), ), ); } @override Widget build(BuildContext context) { return Center( child: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.width * 9.0 / 16.0, child: Video( key: key, controller: controller, controls: (state) { state.widget.controller.player.stream.buffering.listen((event) { if (buffering != event) { setState(() { buffering = event; }); } }); state.widget.controller.player.stream.playing.listen((event) { if (playing != event) { setState(() { playing = event; }); } }); return Scaffold( backgroundColor: Colors.transparent, body: Builder( builder: (ctx) => GestureDetector( // _toggleFullscreen here works with double tap // okay to enter and exit fullscreen as expected onDoubleTap: () { _toggleFullscreen(() { setState(() { isFullscreen = !isFullscreen; }); }); }, onTap: () { _showVideoControls(ctx); }, child: _buildLoadingIndicator(), ), ), ); }, ), ), ); } // for demo only Widget _buildLoadingIndicator() { return Stack( fit: StackFit.expand, children: [ Positioned.fill( child: DecoratedBox( decoration: const BoxDecoration(color: Colors.transparent), child: Center( child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: buffering ? 1.0 : 0.0), duration: const Duration(milliseconds: 150), builder: (ctx, value, child) { if (value > 0.0) { return Opacity( opacity: value, child: child ?? const CircularProgressIndicator(), ); } return const SizedBox.shrink(); }, child: const CircularProgressIndicator(), ), ), ), ), ], ); } } ```
Prince-of-death commented 1 month ago

i am facing the same exact problem in my code too

abdelaziz-mahdy commented 1 month ago

can you provide a working example so i can test with and let you know of a fix?

b2nil commented 1 month ago

@abdelaziz-mahdy hi, I already provided a fully working sample above, just click to expand to copy the code.

abdelaziz-mahdy commented 1 month ago

@abdelaziz-mahdy hi, I already provided a fully working sample above, just click to expand to copy the code.

oh i am sorry. i didnt see it, i will test it when i got free time, and if i have a fix i will let you know

b2nil commented 1 month ago

@abdelaziz-mahdy hi, I already provided a fully working sample above, just click to expand to copy the code.

oh i am sorry. i didnt see it, i will test it when i got free time, and if i have a fix i will let you know

No problem and take your time.

abdelaziz-mahdy commented 4 weeks ago

the problem is the context of the modal sheet, is wrong the code cant find the full screen widget

so the fix should be not using modalsheet, couldnt find a way to pass the right context to the modal sheet, it cant find it anywhere

this is the updated example

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MediaKit.ensureInitialized();
  runApp(const TVApp());
}

class TVApp extends StatelessWidget {
  const TVApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'IPTV Player',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
            seedColor: const Color.fromARGB(255, 47, 99, 89)),
        useMaterial3: true,
      ),
      home: const DemoPlayer(title: 'Demo Mediakit Player'),
    );
  }
}

class DemoPlayer extends StatefulWidget {
  const DemoPlayer({super.key, required this.title});
  final String title;

  @override
  State<DemoPlayer> createState() => _DemoPlayerState();
}

class _DemoPlayerState extends State<DemoPlayer> {
  late final GlobalKey<VideoState> key = GlobalKey<VideoState>();
  late final player = Player();
  late var controller = VideoController(player);

  bool playing = false;
  bool buffering = false;
  bool isFullscreen = false;
  ValueNotifier<bool> showControls = ValueNotifier(false);
  @override
  void initState() {
    super.initState();
    if (mounted) {
      controller = VideoController(player);
      player.open(Media("https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"));
      setState(() {
        isFullscreen = key.currentState?.isFullscreen() ?? false;
      });
    }
  }

  // exitFullscreen not working as expected
  void _toggleFullscreen(VoidCallback onToggle) async {
    await key.currentState?.toggleFullscreen();

    // if (!isFullscreen) {
    //   await key.currentState?.enterFullscreen();
    // } else {
    //   await key.currentState?.exitFullscreen();
    // }

    // tried to set the `isFullscreen` with and without a callback function
    onToggle();

    // setState(() {
    //   isFullscreen = !isFullscreen;
    // });
  }

  void playPause() {
    setState(() {
      controller.player.playOrPause();
      playing = !playing;
    });
  }

  Widget _showVideoControls(BuildContext context) {
    const controlsColor = Colors.white;

    // Using StatefulBuilder here to get access to the inner `setState` method
    // so that the icons updates accordingly with the changes of
    // the states `playing` and `isFullscreen`
    return ValueListenableBuilder(
        valueListenable: showControls,
        builder: (context, value, child) {
          if (!value) {
            return Container();
          }
          return Container(
            color: Colors.black38,
            child: StatefulBuilder(
              builder: (ctx, innerSetState) => LayoutBuilder(
                builder: (layoutCtx, constraints) => SizedBox(
                  child: Center(
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        IconButton(
                          onPressed: () {
                            innerSetState(() {
                              playPause();
                            });
                          },
                          icon: Icon(
                            playing ? Icons.pause : Icons.play_arrow,
                            color: controlsColor,
                          ),
                        ),
                        const Expanded(
                          child: Padding(
                            padding: EdgeInsets.all(20.0),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                              children: [
                                LinearProgressIndicator(),
                              ],
                            ),
                          ),
                        ),
                        IconButton(
                          // The issue is here:
                          //  - okay to enter fullscreen when the button is clicked
                          //  - NOT okay to exit fullscreen when the button is clicked
                          onPressed: () {
                            _toggleFullscreen(() {
                              innerSetState(() {
                                isFullscreen = !isFullscreen;
                              });
                            });
                          },
                          icon: Icon(
                            // The icon toggles as expected when the button is clicked
                            isFullscreen
                                ? Icons.fullscreen_exit
                                : Icons.fullscreen,
                            color: controlsColor,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
        });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.width * 9.0 / 16.0,
        child: Video(
          key: key,
          controller: controller,
          controls: (state) {
            state.widget.controller.player.stream.buffering.listen((event) {
              if (buffering != event) {
                setState(() {
                  buffering = event;
                });
              }
            });

            state.widget.controller.player.stream.playing.listen((event) {
              if (playing != event) {
                setState(() {
                  playing = event;
                });
              }
            });

            return Scaffold(
              backgroundColor: Colors.transparent,
              body: Builder(
                builder: (ctx) => GestureDetector(
                  // _toggleFullscreen here works with double tap
                  // okay to enter and exit fullscreen as expected
                  onDoubleTap: () {
                    _toggleFullscreen(() {
                      setState(() {
                        isFullscreen = !isFullscreen;
                      });
                    });
                  },
                  onTap: () {
                    showControls.value = !showControls.value;
                  },
                  child: Stack(
                    alignment: Alignment.bottomCenter,
                    children: [
                      _buildLoadingIndicator(),
                      Positioned.fill(
                          child: Align(
                              alignment: Alignment.bottomCenter,
                              child: SizedBox(
                                height: 60,
                                child: _showVideoControls(ctx),
                              ))),
                    ],
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }

  // for demo only
  Widget _buildLoadingIndicator() {
    return Stack(
      fit: StackFit.expand,
      children: [
        Positioned.fill(
          child: DecoratedBox(
            decoration: const BoxDecoration(color: Colors.transparent),
            child: Center(
              child: TweenAnimationBuilder(
                tween: Tween<double>(begin: 0.0, end: buffering ? 1.0 : 0.0),
                duration: const Duration(milliseconds: 150),
                builder: (ctx, value, child) {
                  if (value > 0.0) {
                    return Opacity(
                      opacity: value,
                      child: child ?? const CircularProgressIndicator(),
                    );
                  }
                  return const SizedBox.shrink();
                },
                child: const CircularProgressIndicator(),
              ),
            ),
          ),
        ),
      ],
    );
  }
}
b2nil commented 4 weeks ago

@abdelaziz-mahdy Thanks for the effort, it is really appreciated. Value notifier works for me.