flutter-tizen / plugins

Flutter plugins for Tizen
67 stars 47 forks source link

Video Player Resets to Previous Screen Size on URL Update in Fullscreen Mode, On Tizen-TV-Platform #726

Open mwesigwadi opened 3 months ago

mwesigwadi commented 3 months ago

I'm encountering an issue with the video_player_avplay plugin in Flutter-Tizen. I've created a reusable widget for the video player that updates based on the input URL parameter. To achieve this, I use didUpdateWidget to dispose of and reinitialize the player when the URL changes. This player is utilized across multiple screens and listens to a single String provider that updates the URL.

The challenge arises when the player is used on a screen with a fixed-width widget. Upon navigating to a new screen and passing the player's controller as a parameter, the player correctly transitions to fullscreen. However, when I update the URL by setting a new string in the provider, the player unexpectedly resets to the initial size from the previous screen, instead of remaining in fullscreen as intended.

The current setup involves a small screen for previewing a list of videos, with the next route displaying a fullscreen player. The fullscreen player includes a hidden overlay list of videos, which can be displayed on demand for selection.

mwesigwadi commented 3 months ago

Any help on this ?

xiaowei-guan commented 3 months ago

@mwesigwadi Hello, The video_player_avplay plugin's VideoPlayer widget is not based on Texture. The plugin updates native player's position and size by lisen for the VideoPlayer's position and size:

  1. Register addPostFrameCallback when init widget state:
WidgetsBinding.instance.addPostFrameCallback(_afterFrameLayout);
  1. Set the native player's geometry when getting post frame callback
 void _afterFrameLayout(_) {
    if (widget.controller.value.isInitialized) {
      final Rect currentRect = _getCurrentRect();
      if (currentRect != Rect.zero && _playerRect != currentRect) {
        _videoPlayerPlatform.setDisplayGeometry(
          _playerId,
          currentRect.left.toInt(),
          currentRect.top.toInt(),
          currentRect.width.toInt(),
          currentRect.height.toInt(),
        );
        _playerRect = currentRect;
      }
    }
    WidgetsBinding.instance.addPostFrameCallback(_afterFrameLayout);
  }
  1. You should keep only one VideoPlayer widget to hold the Video Controller.
  2. if you change the URL, you need re-create Video controller.
mwesigwadi commented 2 months ago

Hello @xiaowei-guan thanks for the response, however can't this be dynamic like the official video player ?. It would be great if the videoPlayer could span any given space respecting constraints.

mwesigwadi commented 2 months ago

I have tried and the player still gets resized not covering the current screen size.

Checkout my resusable videoplayer widget.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:hotel_iptv/core/constants/application_colors.dart';
import 'package:hotel_iptv/features/videoplayer/application/controllers/video_player_controller.dart';
import 'package:provider/provider.dart';
import 'package:video_player_avplay/video_player.dart';
import 'package:video_player_avplay/video_player_platform_interface.dart';

import '../../../../core/utils/navigation_intents.dart';

class VideoPlayerScreen extends StatefulWidget {
  const VideoPlayerScreen({
    super.key,
    required this.url,
    this.looping = false,
    this.backgroundImage,
  });
  final String url;
  final bool? looping;
  final String? backgroundImage;
  @override
  State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen>
    with SingleTickerProviderStateMixin {
  VideoPlayerController? _controller;

  @override
  void initState() {
    super.initState();
    if (widget.url.isNotEmpty) {
      _initializeController();
    }
  }

  @override
  void didUpdateWidget(covariant VideoPlayerScreen oldWidget) {
    if (oldWidget.url != widget.url) {
      if (_controller != null) {
        _disposeController();
        if (widget.url.isNotEmpty) {
          _initializeController();
        }
      } else {
        if (widget.url.isNotEmpty) {
          _initializeController();
        }
      }
    }

    super.didUpdateWidget(oldWidget);
  }

  void _onVideoPLayerStateChanged() {
    if (_controller == null) return;
    if (_controller!.value.isPlaying) {
      context.read<VideoController>().setIsPlaying(true);
    } else {
      context.read<VideoController>().setIsPlaying(false);
    }
  }

  void _initializeController() {
    _controller = VideoPlayerController.network(
      playerOptions: {},
      widget.url,
      formatHint: VideoFormat.dash,
    )
      ..setLooping(widget.looping!)
      ..initialize().then((_) {
        setState(() {
          _controller?.play();
        });
      });
    _controller?.addListener(_onVideoPLayerStateChanged);
  }

  _disposeController() {
    _controller?.removeListener(_onVideoPLayerStateChanged);
    _controller?.deactivate();
    _controller?.dispose();
  }

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

  @override
  Widget build(BuildContext context) {
    // print(_controller?.value.size);
    return Shortcuts(
      shortcuts: {
        LogicalKeySet(LogicalKeyboardKey.enter): const IntentEnter(),
      },
      child: Actions(
        actions: {
          IntentEnter: CallbackAction<IntentEnter>(
            onInvoke: (IntentEnter intent) =>
                _handleKey(context, LogicalKeyboardKey.enter),
          ),
        },
        child: Scaffold(
            backgroundColor: ApplicationColors.black,
            body: Builder(builder: (context) {
              if (widget.url.isNotEmpty && _controller != null) {
                return !_controller!.value.isInitialized
                    ? Container(
                        width: ScreenUtil().screenWidth,
                        height: ScreenUtil().screenHeight,
                        clipBehavior: Clip.hardEdge,
                        decoration: BoxDecoration(
                            color: Colors.black,
                            image: widget.backgroundImage == null
                                ? null
                                : DecorationImage(
                                    image: NetworkImage(widget
                                            .backgroundImage ??
                                        'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0c/15/83/36/zona-de-aguas.jpg?w=1200&h=-1&s=1'),
                                    fit: BoxFit.fill)),
                      )
                    : VideoPlayer(_controller!);
              } else {
                return Container(
                  width: ScreenUtil().screenWidth,
                  height: ScreenUtil().screenHeight,
                  color: ApplicationColors.black,
                );
              }
            })),
      ),
    );
  }

  void _handleKey(BuildContext context, LogicalKeyboardKey key) {
    if (key == LogicalKeyboardKey.enter) {
      context.read<VideoController>().setIsOverlay(true);
    }
  }
}

`

xiaowei-guan commented 2 months ago

I have tried and the player still gets resized not covering the current screen size.

Checkout my resusable videoplayer widget.

`import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:hotel_iptv/core/constants/application_colors.dart'; import 'package:hotel_iptv/features/videoplayer/application/controllers/video_player_controller.dart'; import 'package:provider/provider.dart'; import 'package:video_player_avplay/video_player.dart'; import 'package:video_player_avplay/video_player_platform_interface.dart';

import '../../../../core/utils/navigation_intents.dart';

class VideoPlayerScreen extends StatefulWidget { const VideoPlayerScreen({ super.key, required this.url, this.looping = false, this.backgroundImage, }); final String url; final bool? looping; final String? backgroundImage; @OverRide State createState() => _VideoPlayerScreenState(); }

class _VideoPlayerScreenState extends State with SingleTickerProviderStateMixin { VideoPlayerController? _controller;

@OverRide void initState() { super.initState(); if (widget.url.isNotEmpty) { _initializeController(); } }

@OverRide void didUpdateWidget(covariant VideoPlayerScreen oldWidget) { if (oldWidget.url != widget.url) { if (_controller != null) { _disposeController(); if (widget.url.isNotEmpty) { _initializeController(); } } else { if (widget.url.isNotEmpty) { _initializeController(); } } }

super.didUpdateWidget(oldWidget);

}

void _onVideoPLayerStateChanged() { if (_controller == null) return; if (_controller!.value.isPlaying) { context.read().setIsPlaying(true); } else { context.read().setIsPlaying(false); } }

void _initializeController() { controller = VideoPlayerController.network( playerOptions: {}, widget.url, formatHint: VideoFormat.dash, ) ..setLooping(widget.looping!) ..initialize().then(() { setState(() { _controller?.play(); }); }); _controller?.addListener(_onVideoPLayerStateChanged); }

_disposeController() { _controller?.removeListener(_onVideoPLayerStateChanged); _controller?.deactivate(); _controller?.dispose(); }

@OverRide void dispose() { _disposeController(); super.dispose(); }

@OverRide Widget build(BuildContext context) { // print(_controller?.value.size); return Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.enter): const IntentEnter(), }, child: Actions( actions: { IntentEnter: CallbackAction( onInvoke: (IntentEnter intent) => _handleKey(context, LogicalKeyboardKey.enter), ), }, child: Scaffold( backgroundColor: ApplicationColors.black, body: Builder(builder: (context) { if (widget.url.isNotEmpty && _controller != null) { return !_controller!.value.isInitialized ? Container( width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, clipBehavior: Clip.hardEdge, decoration: BoxDecoration( color: Colors.black, image: widget.backgroundImage == null ? null : DecorationImage( image: NetworkImage(widget .backgroundImage ?? 'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0c/15/83/36/zona-de-aguas.jpg?w=1200&h=-1&s=1'), fit: BoxFit.fill)), ) : VideoPlayer(_controller!); } else { return Container( width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, color: ApplicationColors.black, ); } })), ), ); }

void _handleKey(BuildContext context, LogicalKeyboardKey key) { if (key == LogicalKeyboardKey.enter) { context.read().setIsOverlay(true); } } } `

Could you please provide a full example code?

mwesigwadi commented 2 months ago

Sure! Here's the widget. It's called dynamically throughout the app, and when provided with a new URL string, it disposes of the current controller and reinitializes a new one. However, even after being reinitialized, the controller still retains the dimensions from the first screen where it was used. For example, if it's initially displayed full-screen, it keeps that size, even if it's later called in a fixed-size widget, which leads to the video overflowing

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:hotel_iptv/core/constants/application_colors.dart';
import 'package:hotel_iptv/features/videoplayer/application/controllers/video_player_controller.dart';
import 'package:provider/provider.dart';
import 'package:video_player_avplay/video_player.dart';
import 'package:video_player_avplay/video_player_platform_interface.dart';

import '../../../../core/utils/navigation_intents.dart';

class VideoPlayerScreen extends StatefulWidget {
  const VideoPlayerScreen({
    super.key,
    required this.url,
    this.looping = false,
    this.backgroundImage,
  });
  final String url;
  final bool? looping;
  final String? backgroundImage;
  @override
  State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen>
    with SingleTickerProviderStateMixin {
  VideoPlayerController? _controller;

  @override
  void initState() {
    super.initState();
    if (widget.url.isNotEmpty) {
      _initializeController();
    }
  }

  @override
  void didUpdateWidget(covariant VideoPlayerScreen oldWidget) {
    if (oldWidget.url != widget.url) {
      if (_controller != null) {
        _disposeController();
        if (widget.url.isNotEmpty) {
          _initializeController();
        }
      } else {
        if (widget.url.isNotEmpty) {
          _initializeController();
        }
      }
    }

    super.didUpdateWidget(oldWidget);
  }

  void _onVideoPLayerStateChanged() {
    if (_controller == null) return;
    if (_controller!.value.isPlaying) {
      context.read<VideoController>().setIsPlaying(true);
    } else {
      context.read<VideoController>().setIsPlaying(false);
    }
  }

  void _initializeController() {
    _controller = VideoPlayerController.network(
      playerOptions: {},
      widget.url,
      formatHint: VideoFormat.dash,
    )
      ..setLooping(widget.looping!)
      ..initialize().then((_) {
        setState(() {
          _controller?.play();
        });
      });
    _controller?.addListener(_onVideoPLayerStateChanged);
  }

  _disposeController() {
    _controller?.removeListener(_onVideoPLayerStateChanged);
    _controller?.deactivate();
    _controller?.dispose();
  }

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

  @override
  Widget build(BuildContext context) {
    return Shortcuts(
      shortcuts: {
        LogicalKeySet(LogicalKeyboardKey.enter): const IntentEnter(),
      },
      child: Actions(
        actions: {
          IntentEnter: CallbackAction<IntentEnter>(
            onInvoke: (IntentEnter intent) =>
                _handleKey(context, LogicalKeyboardKey.enter),
          ),
        },
        child: Scaffold(
            backgroundColor: ApplicationColors.black,
            body: Builder(builder: (context) {
              if (widget.url.isNotEmpty && _controller != null) {
                return !_controller!.value.isInitialized
                    ? Container(
                        width: ScreenUtil().screenWidth,
                        height: ScreenUtil().screenHeight,
                        clipBehavior: Clip.hardEdge,
                        decoration: BoxDecoration(
                            color: Colors.black,
                            image: widget.backgroundImage == null
                                ? null
                                : DecorationImage(
                                    image: NetworkImage(widget
                                            .backgroundImage ??
                                        'https://dynamic-media-cdn.tripadvisor.com/media/photo-o/0c/15/83/36/zona-de-aguas.jpg?w=1200&h=-1&s=1'),
                                    fit: BoxFit.fill)),
                      )
                    : VideoPlayer(_controller!);
              } else {
                return Container(
                  width: ScreenUtil().screenWidth,
                  height: ScreenUtil().screenHeight,
                  color: ApplicationColors.black,
                );
              }
            })),
      ),
    );
  }

  void _handleKey(BuildContext context, LogicalKeyboardKey key) {
    if (key == LogicalKeyboardKey.enter) {
      context.read<VideoController>().setIsOverlay(true);
    }
  }
}

I tried adjusting the layout in the plugin, but I encountered continuous overflow issues. Despite calling the widget within constrained boxes, the video player still takes up the entire screen without respecting the parent-child constraints. Here's the code snippet i used:

void _afterFrameLayout(_) {
  if (widget.controller.value.isInitialized) {
    final Rect currentRect = _getCurrentRect();
    if (currentRect != Rect.zero && _playerRect != currentRect) {
      _videoPlayerPlatform.setDisplayGeometry(
        _playerId,
        0,
        0,
        currentRect.width.toInt(),
        currentRect.height.toInt(),
      );
      _playerRect = currentRect;
    }
  }
  WidgetsBinding.instance.addPostFrameCallback(_afterFrameLayout);
}

Rect _getCurrentRect() {
  final Size screenSize = MediaQuery.of(_videoBoxKey.currentContext!).size;
  final double pixelRatio = WidgetsBinding.instance.window.devicePixelRatio;
  final Size size = screenSize * pixelRatio;
  final Offset offset = Offset.zero;

  return offset & size;
}