Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[video_player_android] Expose correct AspectRatio with rotationCorrection into consideration #166097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Andi1986 opened this issue Mar 27, 2025 · 16 comments · May be fixed by flutter/packages#9199
Labels
c: proposal A detailed proposal for a change to Flutter p: video_player The Video Player plugin P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. platform-android Android applications specifically team-android Owned by Android platform team triaged-android Triaged by Android platform team

Comments

@Andi1986
Copy link

Andi1986 commented Mar 27, 2025

Steps to reproduce

  • Use Galaxy A50
  • create video and play it with the video_player

Expected results

I should see correct aspect ratio

Actual results

Aspect ratio is wrong, see screenshots below
So while 2.9.3 was bad, 2.9.5 is unfortionately worse for galaxy A50

Code sample

Code sample
[Paste your code here]

Screenshots or Video

2.9.3 2.9.5
Image Image

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output

/Users/andreashofmann/code/flutter/bin/flutter doctor --verbose
[✓] Flutter (Channel stable, 3.29.2, on macOS 15.3.2 24D81 darwin-arm64, locale de-DE) [487ms]
• Flutter version 3.29.2 on channel stable at /Users/andreashofmann/code/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision c236373 (vor 2 Wochen), 2025-03-13 16:17:06 -0400
• Engine revision 18b71d6
• Dart version 3.7.2
• DevTools version 2.42.3

[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [1.470ms]
• Android SDK at /Users/andreashofmann/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: flutter config --jdk-dir="path/to/jdk".
• Java version OpenJDK Runtime Environment (build 21.0.5+-13047016-b750.29)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.2) [1.374ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16C5032a
• CocoaPods version 1.16.2

[✓] Chrome - develop for the web [31ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2024.3) [30ms]
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• android-studio-dir = /Applications/Android Studio.app
• Java version OpenJDK Runtime Environment (build 21.0.5+-13047016-b750.29)

[✓] IntelliJ IDEA Community Edition (version 2022.3.2) [29ms]
• IntelliJ at /Applications/IntelliJ IDEA CE.app
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] Connected device (6 available) [6,9s]
• SM A505FN (mobile) • R58MB47Z7FZ • android-arm64 • Android 11 (API 30)
• sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64 • Android 14 (API 34) (emulator)
• iPhone 16 Pro (mobile) • 0986E39E-BDF8-4A29-BA63-C814D50CB09B • ios • com.apple.CoreSimulator.SimRuntime.iOS-18-2 (simulator)
• macOS (desktop) • macos • darwin-arm64 • macOS 15.3.2 24D81 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 15.3.2 24D81 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 134.0.6998.166

[✓] Network resources [269ms]
• All expected network resources are available.

• No issues found!
Process finished with exit code 0

@maheshj01 maheshj01 added the in triage Presently being triaged by the triage team label Mar 28, 2025
@maheshj01
Copy link
Member

Hi @Andi1986,

There is a similar issue describing this case #132934, Please follow up there for further updates, Closing this issue as a duplicate. If you disagree feel free to write in the comments and we will reopen it.

Thank you.

@maheshj01 maheshj01 added r: duplicate Issue is closed as a duplicate of an existing issue and removed in triage Presently being triaged by the triage team labels Mar 28, 2025
@Andi1986
Copy link
Author

Hi @Andi1986,

There is a similar issue describing this case #132934, Please follow up there for further updates, Closing this issue as a duplicate. If you disagree feel free to write in the comments and we will reopen it.

Thank you.

Are you sure its related? Because the other issue is alot older but in 2.9.3 i was not facing any stretch issue, only scale issue

@marvin-kolja
Copy link

marvin-kolja commented Apr 10, 2025

@Andi1986 I experienced a similar issue and would like to check if my findings fit here. It would be awesome if you could share some code and the exact video_player_android versions that were pinned during your tests (in tandem with the video_player versions).

@maheshj01
Copy link
Member

@Andi1986 Thanks for the clarification, Please share a minimal and complete sample code, And is this issue on a particular device or all devices?

@maheshj01 maheshj01 reopened this Apr 10, 2025
@maheshj01 maheshj01 added in triage Presently being triaged by the triage team waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds and removed r: duplicate Issue is closed as a duplicate of an existing issue labels Apr 10, 2025
@Andi1986
Copy link
Author

Andi1986 commented Apr 11, 2025

@maheshj01 i tried to figure out what it could be, maybe the video player is not even totally responsible for this issue, it could be something with the camera plugin instead or the way i am implementing it. i tried to build an example which uses both the camera plugin and the video player plugin in combination.
in this following example, you will notice that capturing video in landscape mode seems to work fine but capturing in portrait mode has issues, you can reproduce it in emulator:

code sample
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const CameraScreen(),
    );
  }
}

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});

  static const routeName = 'camera';

  @override
  State<CameraScreen> createState() => _CameraAppState();
}


class _CameraAppState extends State<CameraScreen> with WidgetsBindingObserver {
  late CameraController _controller;
  late List<CameraDescription> cameras;

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

  Future<CameraController> _requestAndInitCamera() async {
    cameras = await availableCameras();
    var firstCamera = cameras.firstWhere(
            (camera) => camera.lensDirection == CameraLensDirection.back);
    _controller = CameraController(
      firstCamera,
      ResolutionPreset.veryHigh,
    );

    await _controller.initialize();
    return _controller;
  }

  @override
  void dispose() {
    _controller.dispose();
    // KeepScreenOn.turnOff();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => FutureBuilder(
      future: _requestAndInitCamera(),
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const Center(child: CircularProgressIndicator());
        }
        var size = MediaQuery.of(context).size;
        double xScale;
        double yScale;
        if (MediaQuery.of(context).orientation == Orientation.portrait) {
          xScale = size.height / _controller.value.aspectRatio / size.width;
          yScale = 1.0;
        } else {
          xScale = 1.0;
          yScale = size.width / size.height / _controller.value.aspectRatio;
        }
        return Scaffold(
          body: AspectRatio(
            aspectRatio: size.width / size.height,
            child: Transform(
              alignment: Alignment.center,
              transform: Matrix4.diagonal3Values(xScale, yScale, 1),
              child: CameraPreview(_controller),
            ),
          ),
          floatingActionButton: CameraButtons(
              controller: _controller,
              cameras: cameras),
        );
      });

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      await _controller.initialize();
    }
  }
}

class CameraButtons extends StatefulWidget {
  const CameraButtons(
      {super.key,
        required this.controller,
        required this.cameras});

  final CameraController controller;

  final List<CameraDescription> cameras;

  @override
  State<CameraButtons> createState() => _CameraButtonsState();
}

class _CameraButtonsState extends State<CameraButtons> {
  bool recording = false;
  CameraLensDirection _cameraLensDirection = CameraLensDirection.back;

  @override
  Widget build(BuildContext context) =>
      Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
        FloatingActionButton(
          tooltip: 'Kamera wechseln',
          child: Icon(_cameraLensDirection == CameraLensDirection.back
              ? Icons.switch_camera
              : Icons.switch_camera_outlined),
          onPressed: () {
            setState(() => _cameraLensDirection =
            _cameraLensDirection == CameraLensDirection.back
                ? CameraLensDirection.front
                : CameraLensDirection.back);

            var camera = widget.cameras.firstWhere(
                    (camera) => camera.lensDirection == _cameraLensDirection);

            widget.controller.setDescription(camera);
          },
        ),
        if (!recording)
          FloatingActionButton(
            heroTag: 'heroVideoStart',
            onPressed: startRecording,
            tooltip: 'Video aufnehmen',
            child: const Icon(Icons.fiber_manual_record),
          ),
        if (recording)
          FloatingActionButton(
            heroTag: 'heroVideoStop',
            onPressed: stopRecording,
            tooltip: 'Video stoppen',
            child: const Icon(Icons.stop_circle),
          ),
      ]);


  Future<void> startRecording() async {
    await widget.controller.startVideoRecording();
    setState(() {
      recording = true;
    });
  }

  Future<void> stopRecording() async {
    var video = await widget.controller.stopVideoRecording();
    if (!mounted) {
      return;
    }
    await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => Video(
          videoPath: video.path,
        ),
      ),
    );

    setState(() {
      recording = false;
    });
  }
}


class Video extends StatefulWidget {
  const Video({super.key, required this.videoPath});

  final String videoPath;

  @override
  State<Video> createState() => _VideoState();
}

class _VideoState extends State<Video> {
  late VideoPlayerController _controller;

  @override
  void initState() {
    _controller = VideoPlayerController.file(
      File(widget.videoPath),
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
    );
    _controller
      ..addListener(() {
        setState(() {});
      })
      ..setLooping(true)
      ..initialize();
    super.initState();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Vorschau')),
    body: Center(
      child:
      _controller.value.isInitialized
          ? AspectRatio(
        aspectRatio: _controller.value.aspectRatio,
        child: Stack(
          alignment: Alignment.bottomCenter,
          children: <Widget>[
            VideoPlayer(_controller),
            _ControlsOverlay(controller: _controller),
            VideoProgressIndicator(_controller, allowScrubbing: true),
          ],
        ),
      )
          : Container(),
    ),
  );

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

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = <Duration>[
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = <double>[
    0.25,
    0.5,
    1,
    1.5,
    2,
    3,
    5,
    10,
  ];

  final VideoPlayerController controller;

  @override
  Widget build(BuildContext context) => Stack(
    children: <Widget>[
      AnimatedSwitcher(
        duration: const Duration(milliseconds: 50),
        reverseDuration: const Duration(milliseconds: 200),
        child:
        controller.value.isPlaying
            ? const SizedBox.shrink()
            : const ColoredBox(
          color: Colors.black26,
          child: Center(
            child: Icon(
              Icons.play_arrow,
              color: Colors.white,
              size: 100,
              semanticLabel: 'Play',
            ),
          ),
        ),
      ),
      GestureDetector(
        onTap: () {
          controller.value.isPlaying ? controller.pause() : controller.play();
        },
      ),
      Align(
        alignment: Alignment.topLeft,
        child: PopupMenuButton<Duration>(
          initialValue: controller.value.captionOffset,
          tooltip: 'Caption Offset',
          onSelected: controller.setCaptionOffset,
          itemBuilder:
              (context) => <PopupMenuItem<Duration>>[
            for (final Duration offsetDuration in _exampleCaptionOffsets)
              PopupMenuItem<Duration>(
                value: offsetDuration,
                child: Text('${offsetDuration.inMilliseconds}ms'),
              ),
          ],
          child: Padding(
            padding: const EdgeInsets.symmetric(
              // Using less vertical padding as the text is also longer
              // horizontally, so it feels like it would need more spacing
              // horizontally (matching the aspect ratio of the video).
              vertical: 12,
              horizontal: 16,
            ),
            child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
          ),
        ),
      ),
      Align(
        alignment: Alignment.topRight,
        child: PopupMenuButton<double>(
          initialValue: controller.value.playbackSpeed,
          tooltip: 'Playback speed',
          onSelected: controller.setPlaybackSpeed,
          itemBuilder:
              (context) => <PopupMenuItem<double>>[
            for (final double speed in _examplePlaybackRates)
              PopupMenuItem<double>(value: speed, child: Text('${speed}x')),
          ],
          child: Padding(
            padding: const EdgeInsets.symmetric(
              // Using less vertical padding as the text is also longer
              // horizontally, so it feels like it would need more spacing
              // horizontally (matching the aspect ratio of the video).
              vertical: 12,
              horizontal: 16,
            ),
            child: Text('${controller.value.playbackSpeed}x'),
          ),
        ),
      ),
    ],
  );
}

@github-actions github-actions bot removed the waiting for customer response The Flutter team cannot make further progress on this issue until the original reporter responds label Apr 11, 2025
@marvin-kolja
Copy link

@Andi1986 You're not taking the rotationCorrection into account. Same as I did.

AspectRatio(
        aspectRatio: _controller.value.aspectRatio,
        child: Stack(
          alignment: Alignment.bottomCenter,
          children: <Widget>[
            VideoPlayer(_controller),
            _ControlsOverlay(controller: _controller),
            VideoProgressIndicator(_controller, allowScrubbing: true),
          ],
        ),
      )

The width and height returned are the ones before the correction was applied. Thus, the aspect ratio which you're aspecting isn't always the one returned. If the correction is 90 or 270 degrees you'd need to swap the height and width.

I think that's the expected behavior but isn't documented well enough.

@Andi1986
Copy link
Author

Andi1986 commented Apr 13, 2025

thanks alot @marvin-kolja, this is working now, tho it still feels hacky to me and makes me think it should somehow be native by the packages themselves.

AspectRatio(
  aspectRatio:
      _controller.value.rotationCorrection == 90 ||
              _controller.value.rotationCorrection == 270
          ? 1 / _controller.value.aspectRatio
          : _controller.value.aspectRatio,
  child: Stack(
    alignment: Alignment.bottomCenter,
    children: <Widget>[
      VideoPlayer(_controller),
      _ControlsOverlay(controller: _controller),
      VideoProgressIndicator(_controller, allowScrubbing: true),
    ],
  ),
)

@marvin-kolja
Copy link

marvin-kolja commented Apr 14, 2025

It does feel hacky, yes.


Just as a reference to when there were major changes to the rotation correction of the video_player_android package:

In commit flutter/packages@5e03bb1 (video_player_android version 2.7.15) the use of the deprecated VideoSize.unappliedRotationDegrees was replaced with Format.rotationDegrees. The general effort was to fix #154696. As the commit explains, since API 22+ VideoSize.unappliedRotationDegrees returns 0.

Since the main video_player depends on video_player_android: ^2.3.5, I'm unsure how frequently that transitive package updates when upgrading the main package. Some people may get those changes way later.


In my mind, the following things would make sense:

  • Either keep the current behaviour but expose values with the corrected* prefix (e.g. correctedAspectRatio). Or return the corrected values and have the uncorrected values with an uncorrected* prefix.
  • Depending on the changes, add enhanced docs to explain this behaviour on Android and how to deal with it. If the decision is to keep the current values uncorrected, changing examples would prevent people from having this issue. Examples of other packages also don't account for rotation correction, such as the camera package.

Maybe @camsim99 can give some insights?

@maheshj01
Copy link
Member

Given that the aspect ratio can be be correctly applied using rotationCorrection.
I am labelling this as a proposal to provide correctAspectRatio considering the rotationCorrection

code sample
import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const CameraScreen(),
    );
  }
}

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});

  static const routeName = 'camera';

  @override
  State<CameraScreen> createState() => _CameraAppState();
}

class _CameraAppState extends State<CameraScreen> with WidgetsBindingObserver {
  late CameraController _controller;
  late List<CameraDescription> cameras;

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

  Future<CameraController> _requestAndInitCamera() async {
    cameras = await availableCameras();
    var firstCamera = cameras.firstWhere(
      (camera) => camera.lensDirection == CameraLensDirection.back,
    );
    _controller = CameraController(firstCamera, ResolutionPreset.veryHigh);

    await _controller.initialize();
    return _controller;
  }

  @override
  void dispose() {
    _controller.dispose();
    // KeepScreenOn.turnOff();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => FutureBuilder(
    future: _requestAndInitCamera(),
    builder: (context, snapshot) {
      if (snapshot.connectionState != ConnectionState.done) {
        return const Center(child: CircularProgressIndicator());
      }
      var size = MediaQuery.of(context).size;
      double xScale;
      double yScale;
      if (MediaQuery.of(context).orientation == Orientation.portrait) {
        xScale = size.height / _controller.value.aspectRatio / size.width;
        yScale = 1.0;
      } else {
        xScale = 1.0;
        yScale = size.width / size.height / _controller.value.aspectRatio;
      }
      return Scaffold(
        body: AspectRatio(
          aspectRatio: size.width / size.height,
          child: Transform(
            alignment: Alignment.center,
            transform: Matrix4.diagonal3Values(xScale, yScale, 1),
            child: CameraPreview(_controller),
          ),
        ),
        floatingActionButton: CameraButtons(
          controller: _controller,
          cameras: cameras,
        ),
      );
    },
  );

  @override
  Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      await _controller.initialize();
    }
  }
}

class CameraButtons extends StatefulWidget {
  const CameraButtons({
    super.key,
    required this.controller,
    required this.cameras,
  });

  final CameraController controller;

  final List<CameraDescription> cameras;

  @override
  State<CameraButtons> createState() => _CameraButtonsState();
}

class _CameraButtonsState extends State<CameraButtons> {
  bool recording = false;
  CameraLensDirection _cameraLensDirection = CameraLensDirection.back;

  @override
  Widget build(BuildContext context) => Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      FloatingActionButton(
        tooltip: 'Kamera wechseln',
        child: Icon(
          _cameraLensDirection == CameraLensDirection.back
              ? Icons.switch_camera
              : Icons.switch_camera_outlined,
        ),
        onPressed: () {
          setState(
            () =>
                _cameraLensDirection =
                    _cameraLensDirection == CameraLensDirection.back
                        ? CameraLensDirection.front
                        : CameraLensDirection.back,
          );

          var camera = widget.cameras.firstWhere(
            (camera) => camera.lensDirection == _cameraLensDirection,
          );

          widget.controller.setDescription(camera);
        },
      ),
      if (!recording)
        FloatingActionButton(
          heroTag: 'heroVideoStart',
          onPressed: startRecording,
          tooltip: 'Video aufnehmen',
          child: const Icon(Icons.fiber_manual_record),
        ),
      if (recording)
        FloatingActionButton(
          heroTag: 'heroVideoStop',
          onPressed: stopRecording,
          tooltip: 'Video stoppen',
          child: const Icon(Icons.stop_circle),
        ),
    ],
  );

  Future<void> startRecording() async {
    await widget.controller.startVideoRecording();
    setState(() {
      recording = true;
    });
  }

  Future<void> stopRecording() async {
    var video = await widget.controller.stopVideoRecording();
    if (!mounted) {
      return;
    }
    await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => Video(videoPath: video.path)),
    );

    setState(() {
      recording = false;
    });
  }
}

class Video extends StatefulWidget {
  const Video({super.key, required this.videoPath});

  final String videoPath;

  @override
  State<Video> createState() => _VideoState();
}

class _VideoState extends State<Video> {
  late VideoPlayerController _controller;

  @override
  void initState() {
    _controller = VideoPlayerController.file(
      File(widget.videoPath),
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
    );
    _controller
      ..addListener(() {
        setState(() {});
      })
      ..setLooping(true)
      ..initialize();
    super.initState();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Vorschau')),
    body: Center(
      child:
          _controller.value.isInitialized
              ? AspectRatio(
                aspectRatio:
                    _controller.value.rotationCorrection == 90 ||
                            _controller.value.rotationCorrection == 270
                        ? 1 / _controller.value.aspectRatio
                        : _controller.value.aspectRatio,
                child: Stack(
                  alignment: Alignment.bottomCenter,
                  children: <Widget>[
                    VideoPlayer(_controller),
                    _ControlsOverlay(controller: _controller),
                    VideoProgressIndicator(_controller, allowScrubbing: true),
                  ],
                ),
              )
              : Container(),
    ),
  );

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

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = <Duration>[
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = <double>[
    0.25,
    0.5,
    1,
    1.5,
    2,
    3,
    5,
    10,
  ];

  final VideoPlayerController controller;

  @override
  Widget build(BuildContext context) => Stack(
    children: <Widget>[
      AnimatedSwitcher(
        duration: const Duration(milliseconds: 50),
        reverseDuration: const Duration(milliseconds: 200),
        child:
            controller.value.isPlaying
                ? const SizedBox.shrink()
                : const ColoredBox(
                  color: Colors.black26,
                  child: Center(
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.white,
                      size: 100,
                      semanticLabel: 'Play',
                    ),
                  ),
                ),
      ),
      GestureDetector(
        onTap: () {
          controller.value.isPlaying ? controller.pause() : controller.play();
        },
      ),
      Align(
        alignment: Alignment.topLeft,
        child: PopupMenuButton<Duration>(
          initialValue: controller.value.captionOffset,
          tooltip: 'Caption Offset',
          onSelected: controller.setCaptionOffset,
          itemBuilder:
              (context) => <PopupMenuItem<Duration>>[
                for (final Duration offsetDuration in _exampleCaptionOffsets)
                  PopupMenuItem<Duration>(
                    value: offsetDuration,
                    child: Text('${offsetDuration.inMilliseconds}ms'),
                  ),
              ],
          child: Padding(
            padding: const EdgeInsets.symmetric(
              // Using less vertical padding as the text is also longer
              // horizontally, so it feels like it would need more spacing
              // horizontally (matching the aspect ratio of the video).
              vertical: 12,
              horizontal: 16,
            ),
            child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
          ),
        ),
      ),
      Align(
        alignment: Alignment.topRight,
        child: PopupMenuButton<double>(
          initialValue: controller.value.playbackSpeed,
          tooltip: 'Playback speed',
          onSelected: controller.setPlaybackSpeed,
          itemBuilder:
              (context) => <PopupMenuItem<double>>[
                for (final double speed in _examplePlaybackRates)
                  PopupMenuItem<double>(value: speed, child: Text('${speed}x')),
              ],
          child: Padding(
            padding: const EdgeInsets.symmetric(
              // Using less vertical padding as the text is also longer
              // horizontally, so it feels like it would need more spacing
              // horizontally (matching the aspect ratio of the video).
              vertical: 12,
              horizontal: 16,
            ),
            child: Text('${controller.value.playbackSpeed}x'),
          ),
        ),
      ),
    ],
  );
}

@maheshj01 maheshj01 added platform-android Android applications specifically p: video_player The Video Player plugin package flutter/packages repository. See also p: labels. c: proposal A detailed proposal for a change to Flutter team-android Owned by Android platform team and removed in triage Presently being triaged by the triage team labels Apr 15, 2025
@maheshj01 maheshj01 changed the title [video_player_android] video aspect ratio still incorrect after 2.9.5 [video_player_android] Expose correct AspectRatio with rotationCorrection into consideration Apr 15, 2025
@camsim99
Copy link
Contributor

@marvin-kolja Thank you for the clear explanation of potential fixes and the context!

Looks like rotationCorrection was added in flutter/plugins#3820 to fix #60327, so I think using it to correct the aspect ratio is technically a (positive) side effect. 😅

I understand both of the options you presented, though I think having to expose corrected/incorrect values for developers to use to manually correct the aspect ratio is sorta messy. Ideally, we use it to correct the aspect ratio, perhaps at the Dart VideoPlayerController level, but if not the native Android level.

@marvin-kolja
Copy link

marvin-kolja commented Apr 16, 2025

@camsim99 Thank you so much for the insights!

Having another look at the code, I think I found the issue causing this.

Currently, the width and height are switched when the rotation correction is 90 or 270 degrees (first committed here: flutter/plugins@014e373). This was done using the width and height of the VideoFormat. However, in PR flutter/packages#6535, the ExoPlayer was migrated and with it where the width and height are coming from (now VideoSize). This was requested in this comment flutter/packages#6535 (comment) because the video format was unstable (maybe still is?).

Testing on an emulator running Andriod 15.0 (V) - API 35, using a video with 1920x1080 dimensions and a 90-degree rotation, I logged the following:

D/TextureExoPlayerEventListener( 3777): VideoSize: 1080x1920, unappliedRotationDegrees: 0
D/TextureExoPlayerEventListener( 3777): VideoFormat: 1920x1080, rotationDegrees: 90

It seems that the VideoSize contains post-rotation dimensions.

If the VideoSize is kept to get the width and height, I'd suggest removing the switch (Unless VideoSize returns different width and height values on different APIs).

Furthermore, I suppose the behaviour should be similar to the newly added platform view. There the VideoFormat is currently used to get width and height.

@camsim99
Copy link
Contributor

@marvin-kolja Thank you so much for the investigation and clear explanation! That could definitely explain what's going on! I agree3 that we should also investigate the platform view video player to ensure it's working correctly, too.

@camsim99 camsim99 added P2 Important issues not at the top of the work list triaged-android Triaged by Android platform team labels Apr 22, 2025
@marvin-kolja
Copy link

@camsim99 I'll do a little more testing with different Android versions using emulators (I do not own a physical Android device). Then I could create a PR for others to test the changes I deem to be appropriate.

@camsim99
Copy link
Contributor

@marvin-kolja Thank you, that sounds great! I can help test with physical devices when you create a PR.

@suesitran
Copy link

i can help testing this too when PR is up

@marvin-kolja
Copy link

marvin-kolja commented May 4, 2025

Thank you @suesitran and @camsim99. I opened a PR (flutter/packages#9199). Turns out we only needed minimal changes.

Also, the new platform view is fine as it is and doesn't need any changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: proposal A detailed proposal for a change to Flutter p: video_player The Video Player plugin P2 Important issues not at the top of the work list package flutter/packages repository. See also p: labels. platform-android Android applications specifically team-android Owned by Android platform team triaged-android Triaged by Android platform team
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants