Skip to content

[video_player] [macOS] After initialization, the video player doesn't redraw itself #140782

@tvolkert

Description

@tvolkert

Steps to reproduce

  1. Create an app that depends on the video_player plugin and is enabled for macOS
  2. Put two valid video files in known paths
  3. Run the following app on macOS, replacing the fake video paths below with your real video paths
  4. Select either of the two videos (represented by placeholder widgets) in the list view
  5. Click the main area above the list view to play the video
  6. Select the other video in the list view
import 'dart:io' as io;

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

void main() {
  runApp(const MaterialApp(
    title: 'Issue reproduction',
    home: IssueHome(
      videos: <String>[
        '/path/to/first/video.mp4',
        '/path/to/second/video.mp4',
      ],
    ),
  ));
}

class IssueHome extends StatefulWidget {
  const IssueHome({super.key, required this.videos});

  final List<String> videos;

  @override
  State<IssueHome> createState() => _IssueHomeState();
}

class _IssueHomeState extends State<IssueHome> {
  int? selectedIndex;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            child: selectedIndex == null
                ? Container()
                : VideoArea(item:  widget.videos[selectedIndex!]),
          ),
          const Divider(height: 1),
          SizedBox(
            height: 175,
            child: ListView.builder(
              itemExtent: 175,
              scrollDirection: Axis.horizontal,
              itemCount: widget.videos.length,
              itemBuilder: (BuildContext context, int index) {
                return GestureDetector(
                  onTap: () => setState(() => selectedIndex = index),
                  child: ItemSelector(
                    index: index,
                    isSelected: selectedIndex == index,
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class ItemSelector extends StatelessWidget {
  ItemSelector({
    required this.index,
    required this.isSelected,
  }) : super(key: ValueKey<int>(index));

  final int index;
  final bool isSelected;

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.passthrough,
      children: <Widget>[
        Placeholder(
          child: Text(index.toString()),
        ),
        if (isSelected)
          DecoratedBox(
            decoration: BoxDecoration(border: Border.all(width: 10, color: Colors.white)),
            child: const ColoredBox(color: Color(0x440000ff)),
          ),
      ],
    );
  }
}

class VideoArea extends StatefulWidget {
  const VideoArea({
    super.key,
    required this.item,
  });

  final String item;

  @override
  State<VideoArea> createState() => _VideoAreaState();
}

class _VideoAreaState extends State<VideoArea> {
  late VideoPlayerController _videoController;

  void _handlePlayPauseVideo() {
    setState(() {
      if (_videoController.value.isPlaying) {
        _videoController.pause();
      } else {
        _videoController.play();
      }
    });
  }

  void _initializeVideoController() {
    _videoController = VideoPlayerController.file(io.File(widget.item));
    _videoController.setLooping(true);
    _videoController.initialize().then((void _) {
      setState(() {});
    });
  }

  @override
  void initState() {
    super.initState();
    _initializeVideoController();
  }

  @override
  void didUpdateWidget(covariant VideoArea oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.item != oldWidget.item) {
      _videoController.pause();
      _videoController.dispose();
      _initializeVideoController();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    if (!_videoController.value.isInitialized) {
      return Container();
    } else {
      return GestureDetector(
        onTap: _handlePlayPauseVideo,
        child: Center(
          child: AspectRatio(
            aspectRatio: _videoController.value.aspectRatio,
            child: VideoPlayer(_videoController),
          ),
        ),
      );
    }
  }
}

Expected behavior

When you select a video from the list view, you expect to see the first frame of the video rendered as a preview.

Actual behavior

When you select a video from the list view, the main video area remains white with no video preview.... until you switch application windows, at which point the video preview shows.

This can be seen in the following screen capture:

issue

Diagnosis

The following line triggers a repaint in Flutter:

_videoController.initialize().then((void _) {
  setState(() {});
});

... but that repaint isn't causing the surface associated with the native video player plugin to also repaint. Note that this does not seem to be an off-by-one-frame issue between Flutter and the video player plugin, because the issue still exists even if I register some forced frames like so:

_videoController.initialize().then((void _) {
  setState(() {});
  SchedulerBinding.instance.scheduleForcedFrame();
  SchedulerBinding.instance.scheduleFrameCallback((timeStamp) {
    setState(() {});
    SchedulerBinding.instance.scheduleForcedFrame();
    SchedulerBinding.instance.scheduleFrameCallback((timeStamp) {
      setState(() {});
    });
  });
});

Info

macOS version: Sonoma 14.1 (23B2073)
video_player plugin version: 2.8.1
video_player_avfoundation plugin version: 2.5.3.

$ flutter doctor -v
[✓] Flutter (Channel main, 3.18.0-18.0.pre.46, on macOS 14.1 23B2073 darwin-arm64, locale en-US)
    • Flutter version 3.18.0-18.0.pre.46 on channel main at /Users/tvolkert/project/flutter/flutter
    • Upstream repository [email protected]:flutter/flutter.git
    • Framework revision 7c9c1705e8 (2 days ago), 2023-12-29 07:40:37 -0500
    • Engine revision ea4fb2cb94
    • Dart version 3.3.0 (build 3.3.0-272.0.dev)
    • DevTools version 2.31.0-dev.0

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/tvolkert/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.0.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15A507
    • CocoaPods version 1.14.3

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

[✓] Android Studio (version 2023.1)
    • 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
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)

[✓] VS Code (version 1.85.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.80.0

Metadata

Metadata

Labels

P2Important issues not at the top of the work lista: desktopRunning on desktopa: videoVideo playbackfyi-ecosystemFor the attention of Ecosystem teamp: video_playerThe Video Player pluginpackageflutter/packages repository. See also p: labels.platform-macBuilding on or for macOS specificallyteam-macosOwned by the macOS platform team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions