youtube_player_flutter
youtube_player_flutter copied to clipboard
Thumbnails and video itself are zoomed when in fullscreen / landscape mode
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
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
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?
@dnth have you got any solution?
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 AspectRatio
and the paddind depend on if the screen is full or not .
@kareemalkoul this solution is not working for me can you suggest something else
Facing same issue on latest version youtube_player_flutter: ^8.1.0 Any update on this issue so far?
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,