sarbagyastha / youtube_player_flutter

A Flutter plugin for inline playback or streaming of YouTube videos using the official iFrame Player API.
https://youtube.sarbagyastha.com.np
BSD 3-Clause "New" or "Revised" License
690 stars 766 forks source link

Thumbnails and video itself are zoomed when in fullscreen / landscape mode #294

Open fjmcassoni opened 4 years ago

fjmcassoni commented 4 years ago

Describe the bug Thumbnails and video itself are zoomed when in fullscreen / landscape mode.

To Reproduce Simply rotate to landscape and observe how thumbnail and video are a bit zoomed not respecting their original size.

Expected behavior In my opinion not respecting original thumbnail and video size is an undesirable behaviour as part of image and video go beyond screen boundaries.

I did the following changes to fix (not knowing original motivations for scaling though):

For the thumbnail (removed setting Container width): @override Widget build(BuildContext context) { return Material( elevation: 0, color: Colors.black, child: InheritedYoutubePlayer( controller: controller, child: Container( color: Colors.black, //Next by line commented by fjmcassoni so thumbnail is not zoomed //width: widget.width ?? MediaQuery.of(context).size.width, ...

For the video (removed scaling factor): Widget _buildPlayer({Widget errorWidget}) { return AspectRatio( aspectRatio: _aspectRatio, child: Stack( fit: StackFit.expand, overflow: Overflow.visible, children: [ Transform.scale( *//Next by line commented by fjmcassoni so video is not zoomed //scale: controller.value.isFullScreen // ? (1 / _aspectRatio MediaQuery.of(context).size.width) / // MediaQuery.of(context).size.height // : 1**, scale:1, child: RawYoutubePlayer( key: widget.key, ...

Screenshots Sample video where these occurs: https://youtu.be/1zs7TkEPPO0

Technical Details:

Additional context N/A

Nico04 commented 3 years ago

I have the same issue. I found a workaround that works ok for the video (not perfect), but doesn't fix the thumbnail.

Wrap the whole page with a LayoutBuilder, and set aspectRatio: constrains.maxHeight / constrains.maxWidth

dnth commented 2 years ago

I have the same issue. I found a workaround that works ok for the video (not perfect), but doesn't fix the thumbnail.

Wrap the whole page with a LayoutBuilder, and set aspectRatio: constrains.maxHeight / constrains.maxWidth

This works for me. The video is less cropped. But I still notice a little cropped out part at the top of the video. Did anyone find anything that works?

anup-geeky commented 2 years ago

@dnth have you got any solution?

kareemalkoul commented 2 years ago

i found sloution not very perfect but sloved the problem i have the package's version is 8.0.0 and edit in file youtube_player_flutter-8.0.0\lib\src\widgets\youtube_player_builder.dart


import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';

/// A wrapper for [YoutubePlayer].
class YoutubePlayerBuilder extends StatefulWidget {
  /// The actual [YoutubePlayer].
  final YoutubePlayer player;

  /// Builds the widget below this [builder].
  final Widget Function(BuildContext, Widget) builder;

  /// Callback to notify that the player has entered fullscreen.
  final VoidCallback? onEnterFullScreen;

  /// Callback to notify that the player has exited fullscreen.
  final VoidCallback? onExitFullScreen;

  /// Builder for [YoutubePlayer] that supports switching between fullscreen and normal mode.
  const YoutubePlayerBuilder({
    Key? key,
    required this.player,
    required this.builder,
    this.onEnterFullScreen,
    this.onExitFullScreen,
  }) : super(key: key);

  @override
  _YoutubePlayerBuilderState createState() => _YoutubePlayerBuilderState();
}

class _YoutubePlayerBuilderState extends State<YoutubePlayerBuilder>
    with WidgetsBindingObserver {
  final GlobalKey playerKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance?.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance?.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeMetrics() {
    final physicalSize = SchedulerBinding.instance?.window.physicalSize;
    final controller = widget.player.controller;
    if (physicalSize != null && physicalSize.width > physicalSize.height) {
      controller.updateValue(controller.value.copyWith(isFullScreen: true));
      SystemChrome.setEnabledSystemUIOverlays([]);
      widget.onEnterFullScreen?.call();
    } else {
      controller.updateValue(controller.value.copyWith(isFullScreen: false));
      SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
      widget.onExitFullScreen?.call();
    }
    super.didChangeMetrics();
  }

  @override
  Widget build(BuildContext context) {
    final _player = Container(
      key: playerKey,
      child: WillPopScope(
        onWillPop: () async {
          final controller = widget.player.controller;
          if (controller.value.isFullScreen) {
            widget.player.controller.toggleFullScreenMode();
            return false;
          }
          return true;
        },
        child: widget.player,
      ),
    );
    final child = widget.builder(context, _player);
    return OrientationBuilder(
      builder: (context, orientation) => orientation == Orientation.portrait
          ? child
          : Padding(child: _player, padding: EdgeInsets.all(10)),
    );
  }
}

and edit in file youtube_player_flutter-8.0.0\lib\src\player\youtube_player.dart

// Copyright 2020 Sarbagya Dhaubanjar. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';

import '../enums/thumbnail_quality.dart';
import '../utils/errors.dart';
import '../utils/youtube_meta_data.dart';
import '../utils/youtube_player_controller.dart';
import '../utils/youtube_player_flags.dart';
import '../widgets/widgets.dart';
import 'raw_youtube_player.dart';

/// A widget to play or stream YouTube videos using the official [YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference).
///
/// In order to play live videos, set `isLive` property to true in [YoutubePlayerFlags].
///
///
/// Using YoutubePlayer widget:
///
/// ```dart
/// YoutubePlayer(
///    context: context,
///    initialVideoId: "iLnmTe5Q2Qw",
///    flags: YoutubePlayerFlags(
///      autoPlay: true,
///      showVideoProgressIndicator: true,
///    ),
///    videoProgressIndicatorColor: Colors.amber,
///    progressColors: ProgressColors(
///      playedColor: Colors.amber,
///      handleColor: Colors.amberAccent,
///    ),
///    onPlayerInitialized: (controller) {
///      _controller = controller..addListener(listener);
///    },
///)
/// ```
///
class YoutubePlayer extends StatefulWidget {
  /// Sets [Key] as an identification to underlying web view associated to the player.
  final Key? key;

  /// A [YoutubePlayerController] to control the player.
  final YoutubePlayerController controller;

  /// {@template youtube_player_flutter.width}
  /// Defines the width of the player.
  ///
  /// Default is devices's width.
  /// {@endtemplate}
  final double? width;

  /// {@template youtube_player_flutter.aspectRatio}
  /// Defines the aspect ratio to be assigned to the player. This property along with [width] calculates the player size.
  ///
  /// Default is 16 / 9.
  /// {@endtemplate}
  final double aspectRatio;

  /// {@template youtube_player_flutter.controlsTimeOut}
  /// The duration for which controls in the player will be visible.
  ///
  /// Default is 3 seconds.
  /// {@endtemplate}
  final Duration controlsTimeOut;

  /// {@template youtube_player_flutter.bufferIndicator}
  /// Overrides the default buffering indicator for the player.
  /// {@endtemplate}
  final Widget? bufferIndicator;

  /// {@template youtube_player_flutter.progressColors}
  /// Overrides default colors of the progress bar, takes [ProgressColors].
  /// {@endtemplate}
  final ProgressBarColors progressColors;

  /// {@template youtube_player_flutter.progressIndicatorColor}
  /// Overrides default color of progress indicator shown below the player(if enabled).
  /// {@endtemplate}
  final Color progressIndicatorColor;

  /// {@template youtube_player_flutter.onReady}
  /// Called when player is ready to perform control methods like:
  /// play(), pause(), load(), cue(), etc.
  /// {@endtemplate}
  final VoidCallback? onReady;

  /// {@template youtube_player_flutter.onEnded}
  /// Called when player had ended playing a video.
  ///
  /// Returns [YoutubeMetaData] for the video that has just ended playing.
  /// {@endtemplate}
  final void Function(YoutubeMetaData metaData)? onEnded;

  /// {@template youtube_player_flutter.liveUIColor}
  /// Overrides color of Live UI when enabled.
  /// {@endtemplate}
  final Color liveUIColor;

  /// {@template youtube_player_flutter.topActions}
  /// Adds custom top bar widgets.
  /// {@endtemplate}
  final List<Widget>? topActions;

  /// {@template youtube_player_flutter.bottomActions}
  /// Adds custom bottom bar widgets.
  /// {@endtemplate}
  final List<Widget>? bottomActions;

  /// {@template youtube_player_flutter.actionsPadding}
  /// Defines padding for [topActions] and [bottomActions].
  ///
  /// Default is EdgeInsets.all(8.0).
  /// {@endtemplate}
  final EdgeInsetsGeometry actionsPadding;

  /// {@template youtube_player_flutter.thumbnail}
  /// Thumbnail to show when player is loading.
  ///
  /// If not set, default thumbnail of the video is shown.
  /// {@endtemplate}
  final Widget? thumbnail;

  /// {@template youtube_player_flutter.showVideoProgressIndicator}
  /// Defines whether to show or hide progress indicator below the player.
  ///
  /// Default is false.
  /// {@endtemplate}
  final bool showVideoProgressIndicator;

  /// Creates [YoutubePlayer] widget.
  const YoutubePlayer({
    this.key,
    required this.controller,
    this.width,
    this.aspectRatio = 16 / 9,
    this.controlsTimeOut = const Duration(seconds: 3),
    this.bufferIndicator,
    Color? progressIndicatorColor,
    ProgressBarColors? progressColors,
    this.onReady,
    this.onEnded,
    this.liveUIColor = Colors.red,
    this.topActions,
    this.bottomActions,
    this.actionsPadding = const EdgeInsets.all(8.0),
    this.thumbnail,
    this.showVideoProgressIndicator = false,
  })  : progressColors = progressColors ?? const ProgressBarColors(),
        progressIndicatorColor = progressIndicatorColor ?? Colors.red;

  /// Converts fully qualified YouTube Url to video id.
  ///
  /// If videoId is passed as url then no conversion is done.
  static String? convertUrlToId(String url, {bool trimWhitespaces = true}) {
    if (!url.contains("http") && (url.length == 11)) return url;
    if (trimWhitespaces) url = url.trim();

    for (var exp in [
      RegExp(
          r"^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(
          r"^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$"),
      RegExp(r"^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$")
    ]) {
      Match? match = exp.firstMatch(url);
      if (match != null && match.groupCount >= 1) return match.group(1);
    }

    return null;
  }

  /// Grabs YouTube video's thumbnail for provided video id.
  static String getThumbnail({
    required String videoId,
    String quality = ThumbnailQuality.standard,
    bool webp = true,
  }) =>
      webp
          ? 'https://i3.ytimg.com/vi_webp/$videoId/$quality.webp'
          : 'https://i3.ytimg.com/vi/$videoId/$quality.jpg';

  @override
  _YoutubePlayerState createState() => _YoutubePlayerState();
}

class _YoutubePlayerState extends State<YoutubePlayer> {
  late YoutubePlayerController controller;

  late double _aspectRatio;
  bool _initialLoad = true;

  @override
  void initState() {
    super.initState();
    controller = widget.controller..addListener(listener);
    _aspectRatio = widget.aspectRatio;
  }

  @override
  void didUpdateWidget(YoutubePlayer oldWidget) {
    super.didUpdateWidget(oldWidget);
    oldWidget.controller.removeListener(listener);
    widget.controller.addListener(listener);
  }

  void listener() async {
    if (controller.value.isReady && _initialLoad) {
      _initialLoad = false;
      if (controller.flags.autoPlay) controller.play();
      if (controller.flags.mute) controller.mute();
      widget.onReady?.call();
      if (controller.flags.controlsVisibleAtStart) {
        controller.updateValue(
          controller.value.copyWith(isControlsVisible: true),
        );
      }
    }
    if (mounted) setState(() {});
  }

  @override
  void dispose() {
    controller.removeListener(listener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) => Material(
        elevation: 0,
        color: Colors.black,
        child: InheritedYoutubePlayer(
          controller: controller,
          child: Container(
            color: Colors.black,
            width: widget.width ?? constraints.maxWidth,
            child: _buildPlayer(
              errorWidget: Container(
                width: widget.width ?? constraints.maxWidth,
                color: Colors.black87,
                padding: const EdgeInsets.symmetric(
                    horizontal: 40.0, vertical: 20.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Row(
                      children: [
                        const Icon(
                          Icons.error_outline,
                          color: Colors.white,
                        ),
                        const SizedBox(width: 5.0),
                        Expanded(
                          child: Text(
                            errorString(
                              controller.value.errorCode,
                              videoId: controller.metadata.videoId.isNotEmpty
                                  ? controller.metadata.videoId
                                  : controller.initialVideoId,
                            ),
                            style: const TextStyle(
                              color: Colors.white,
                              fontWeight: FontWeight.w300,
                              fontSize: 15.0,
                            ),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 16.0),
                    Text(
                      'Error Code: ${controller.value.errorCode}',
                      style: const TextStyle(
                        color: Colors.grey,
                        fontWeight: FontWeight.w300,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildPlayer({required Widget errorWidget}) {
    return LayoutBuilder(
      builder: (context, constraints) => Padding(
        padding: EdgeInsets.symmetric(
            horizontal: controller.value.isFullScreen ? 20.0 : 0,
            vertical: controller.value.isFullScreen ? 20.0 : 0),
        child: AspectRatio(
          aspectRatio: _aspectRatio,
          child: Stack(
            fit: StackFit.expand,
            clipBehavior: Clip.none,
            children: [
              Transform.scale(
                scale: controller.value.isFullScreen
                    ? (1 / _aspectRatio * (constraints.maxWidth)) /
                        constraints.maxHeight
                    : 1,
                child: RawYoutubePlayer(
                  key: widget.key,
                  onEnded: (YoutubeMetaData metaData) {
                    if (controller.flags.loop) {
                      controller.load(controller.metadata.videoId,
                          startAt: controller.flags.startAt,
                          endAt: controller.flags.endAt);
                    }

                    widget.onEnded?.call(metaData);
                  },
                ),
              ),
              if (!controller.flags.hideThumbnail)
                AnimatedOpacity(
                  opacity: controller.value.isPlaying ? 0 : 1,
                  duration: const Duration(milliseconds: 300),
                  child: widget.thumbnail ?? _thumbnail,
                ),
              if (!controller.value.isFullScreen &&
                  !controller.flags.hideControls &&
                  controller.value.position >
                      const Duration(milliseconds: 100) &&
                  !controller.value.isControlsVisible &&
                  widget.showVideoProgressIndicator &&
                  !controller.flags.isLive)
                Positioned(
                  bottom: -7.0,
                  left: -7.0,
                  right: -7.0,
                  child: IgnorePointer(
                    ignoring: true,
                    child: ProgressBar(
                      colors: widget.progressColors.copyWith(
                        handleColor: Colors.transparent,
                      ),
                    ),
                  ),
                ),
              if (!controller.flags.hideControls) ...[
                TouchShutter(
                  disableDragSeek: controller.flags.disableDragSeek,
                  timeOut: widget.controlsTimeOut,
                ),
                Positioned(
                  bottom: 0,
                  left: 0,
                  right: 0,
                  child: AnimatedOpacity(
                    opacity: !controller.flags.hideControls &&
                            controller.value.isControlsVisible
                        ? 1
                        : 0,
                    duration: const Duration(milliseconds: 300),
                    child: controller.flags.isLive
                        ? LiveBottomBar(liveUIColor: widget.liveUIColor)
                        : Padding(
                            padding: widget.bottomActions == null
                                ? const EdgeInsets.all(0.0)
                                : widget.actionsPadding,
                            child: Row(
                              children: widget.bottomActions ??
                                  [
                                    const SizedBox(width: 14.0),
                                    CurrentPosition(),
                                    const SizedBox(width: 8.0),
                                    ProgressBar(
                                      isExpanded: true,
                                      colors: widget.progressColors,
                                    ),
                                    RemainingDuration(),
                                    const PlaybackSpeedButton(),
                                    FullScreenButton(),
                                  ],
                            ),
                          ),
                  ),
                ),
                Positioned(
                  top: 0,
                  left: 0,
                  right: 0,
                  child: AnimatedOpacity(
                    opacity: !controller.flags.hideControls &&
                            controller.value.isControlsVisible
                        ? 1
                        : 0,
                    duration: const Duration(milliseconds: 300),
                    child: Padding(
                      padding: widget.actionsPadding,
                      child: Row(
                        children: widget.topActions ?? [Container()],
                      ),
                    ),
                  ),
                ),
              ],
              if (!controller.flags.hideControls)
                Center(
                  child: PlayPauseButton(),
                ),
              if (controller.value.hasError) errorWidget,
            ],
          ),
        ),
      ),
    );
  }

  Widget get _thumbnail => Image.network(
        YoutubePlayer.getThumbnail(
          videoId: controller.metadata.videoId.isEmpty
              ? controller.initialVideoId
              : controller.metadata.videoId,
        ),
        fit: BoxFit.cover,
        loadingBuilder: (_, child, progress) =>
            progress == null ? child : Container(color: Colors.black),
        errorBuilder: (context, _, __) => Image.network(
          YoutubePlayer.getThumbnail(
            videoId: controller.metadata.videoId.isEmpty
                ? controller.initialVideoId
                : controller.metadata.videoId,
            webp: false,
          ),
          fit: BoxFit.cover,
          loadingBuilder: (_, child, progress) =>
              progress == null ? child : Container(color: Colors.black),
          errorBuilder: (context, _, __) => Container(),
        ),
      );
}

in simple way i just add padding at line 84 ,it is warped the _player. and at line 293, it is warped the AspectRatioand the paddind depend on if the screen is full or not .

jaskiratAtNexG commented 2 years ago

@kareemalkoul this solution is not working for me can you suggest something else

neeluagrawal04 commented 2 years ago

Facing same issue on latest version youtube_player_flutter: ^8.1.0 Any update on this issue so far?

a01r066 commented 7 months ago

I only changed the scale to 0.75 if isFullScreen then it fixed the problem: Transform.scale( scale: controller.value.isFullScreen ? ((1 / _aspectRatio MediaQuery.of(context).size.width) / MediaQuery.of(context).size.height) 0.75 : 1,

dr0g0 commented 2 weeks ago

Describe the bug Thumbnails and video itself are zoomed when in fullscreen / landscape mode.

To Reproduce Simply rotate to landscape and observe how thumbnail and video are a bit zoomed not respecting their original size.

Expected behavior In my opinion not respecting original thumbnail and video size is an undesirable behaviour as part of image and video go beyond screen boundaries.

I did the following changes to fix (not knowing original motivations for scaling though):

For the thumbnail (removed setting Container width): @OverRide Widget build(BuildContext context) { return Material( elevation: 0, color: Colors.black, child: InheritedYoutubePlayer( controller: controller, child: Container( color: Colors.black, //Next by line commented by fjmcassoni so thumbnail is not zoomed //width: widget.width ?? MediaQuery.of(context).size.width, ...

For the video (removed scaling factor): Widget _buildPlayer({Widget errorWidget}) { return AspectRatio( aspectRatio: _aspectRatio, child: Stack( fit: StackFit.expand, overflow: Overflow.visible, children: [ Transform.scale( *//Next by line commented by fjmcassoni so video is not zoomed //scale: controller.value.isFullScreen // ? (1 / _aspectRatio MediaQuery.of(context).size.width) / // MediaQuery.of(context).size.height // : 1**, scale:1, child: RawYoutubePlayer( key: widget.key, ...

Screenshots Sample video where these occurs: https://youtu.be/1zs7TkEPPO0

Technical Details:

  • Device: Motorola One
  • OS: Android 10
  • Version: QPKS30.54-22-9

Additional context N/A

Use MediaQuery,

YoutubePlayer( controller: _controller, aspectRatio: MediaQuery.of(context).size.height/MediaQuery.of(context).size.width .....